Praktiska labb Language Understanding med återkommande nätverk
Observera att den här självstudien kräver den senaste huvudversionen eller den kommande CNTK 1.7.1 som snart kommer att släppas.
Det här praktiska labbet visar hur du implementerar ett återkommande nätverk för att bearbeta text för ATIS-uppgifter (Air Travel Information Services) för platstaggning och avsiktsklassificering. Vi börjar med en enkel inbäddning följt av en återkommande LSTM. Sedan utökar vi den så att den inkluderar grannord och körs dubbelriktat. Slutligen omvandlar vi det här systemet till en avsiktsklassificerare.
De tekniker som du kommer att öva på är:
- modellbeskrivning genom att skapa lagerblock i stället för att skriva formler
- skapa ett eget lagerblock
- variabler med olika sekvenslängder i samma nätverk
- parallell träning
Vi antar att du är bekant med grunderna i djupinlärning och dessa specifika begrepp:
- återkommande nätverk (Wikipedia-sida)
- inbäddning av text (Wikipedia-sida)
Förutsättningar
Vi antar att du redan har installerat CNTK och kan köra CNTK-kommandot. Den här självstudien hölls på KDD 2016 och kräver en ny version, se här för installationsinstruktioner. Du kan bara följa anvisningarna för att ladda ned ett binärt installationspaket från den sidan.
Ladda sedan ned ett ZIP-arkiv (cirka 12 MB): Klicka på den här länken och sedan på knappen Ladda ned.
Arkivet innehåller filerna för den här självstudien. Arkivera och ställ in arbetskatalogen på SLUHandsOn
.
Filerna som du kommer att arbeta med är:
SLUHandsOn.cntk
: DEN CNTK-konfigurationsfil som vi kommer att introducera nedan och arbeta med.slu.forward.nobn.cmf
,slu.forward.cmf
,slu.forward.lookahead.cmf
ochslu.forward.backward.cmf
: Förtränade modeller som är resultatet av de respektive konfigurationer som vi utvecklar under den här självstudien.atis.train.ctf
ochatis.test.ctf
: Tränings- och test corpus, som redan har konverterats till CNTK Text Format (CTF).
Slutligen rekommenderar vi starkt att du kör detta på en dator med en kompatibel CUDA-kompatibel GPU. Djupinlärning utan GPU:er är inte roligt.
Uppgifts- och modellstruktur
Den uppgift som vi vill hantera i den här självstudien är platstaggning. Vi använder ATIS-corpus. ATIS innehåller frågor från människor och datorer från domänen för Air Travel Information Services, och vår uppgift är att kommentera (tagga) varje ord i en fråga om det tillhör ett specifikt informationsobjekt (fack) och vilket.
Data i arbetsmappen har redan konverterats till "CNTK-textformat". Nu ska vi titta på ett exempel från testuppsättningsfilen atis.test.ctf
:
19 |S0 178:1 |# BOS |S1 14:1 |# flight |S2 128:1 |# O
19 |S0 770:1 |# show |S2 128:1 |# O
19 |S0 429:1 |# flights |S2 128:1 |# O
19 |S0 444:1 |# from |S2 128:1 |# O
19 |S0 272:1 |# burbank |S2 48:1 |# B-fromloc.city_name
19 |S0 851:1 |# to |S2 128:1 |# O
19 |S0 789:1 |# st. |S2 78:1 |# B-toloc.city_name
19 |S0 564:1 |# louis |S2 125:1 |# I-toloc.city_name
19 |S0 654:1 |# on |S2 128:1 |# O
19 |S0 601:1 |# monday |S2 26:1 |# B-depart_date.day_name
19 |S0 179:1 |# EOS |S2 128:1 |# O
Den här filen har 7 kolumner:
- ett sekvens-ID (19). Det finns 11 poster med det här sekvens-ID:t. Det innebär att sekvens 19 består av 11 token.
- column
S0
, som innehåller numeriska ordindex; - en kommentarskolumn som anges av
#
, så att en mänsklig läsare kan veta vad det numeriska ordindexet står för. Kommentarskolumner ignoreras av systemet.BOS
ochEOS
är särskilda ord för att beteckna början och slutet av meningen, respektive; - kolumnen
S1
är en avsiktsetikett som vi endast kommer att använda i den sista delen av självstudien. - en annan kommentarskolumn som visar den läsbara etiketten för det numeriska avsiktsindexet.
- kolumn
S2
är facketiketten, representerad som ett numeriskt index, och - en annan kommentarskolumn som visar den läsbara etiketten för det numeriska etikettindexet.
Uppgiften för det neurala nätverket är att titta på frågan (kolumnen S0
) och förutsäga facketiketten (kolumn S2
).
Som du ser tilldelas varje ord i indata antingen en tom etikett O
eller en facketikett som börjar med B-
för det första ordet, och med I-
för ytterligare ord i följd som tillhör samma fack.
Modellen vi använder är en återkommande modell som består av ett inbäddningslager, en återkommande LSTM-cell och ett tätt lager för att beräkna de bakre sannolikheterna:
slot label "O" "O" "O" "O" "B-fromloc.city_name"
^ ^ ^ ^ ^
| | | | |
+-------+ +-------+ +-------+ +-------+ +-------+
| Dense | | Dense | | Dense | | Dense | | Dense | ...
+-------+ +-------+ +-------+ +-------+ +-------+
^ ^ ^ ^ ^
| | | | |
+------+ +------+ +------+ +------+ +------+
0 -->| LSTM |-->| LSTM |-->| LSTM |-->| LSTM |-->| LSTM |-->...
+------+ +------+ +------+ +------+ +------+
^ ^ ^ ^ ^
| | | | |
+-------+ +-------+ +-------+ +-------+ +-------+
| Embed | | Embed | | Embed | | Embed | | Embed | ...
+-------+ +-------+ +-------+ +-------+ +-------+
^ ^ ^ ^ ^
| | | | |
w ------>+--------->+--------->+--------->+--------->+------...
BOS "show" "flights" "from" "burbank"
Eller som en CNTK-nätverksbeskrivning. Ta en snabb titt och matcha den med beskrivningen ovan:
model = Sequential (
EmbeddingLayer {150} :
RecurrentLSTMLayer {300} :
DenseLayer {labelDim}
)
Beskrivningar av dessa funktioner finns på: Sequential()
, EmbeddingLayer{}
, RecurrentLSTMLayer{}
, och DenseLayer{}
CNTK-konfiguration
Konfigurationsfil
För att träna och testa en modell i CNTK måste vi tillhandahålla en konfigurationsfil som talar om för CNTK vilka åtgärder du vill köra (command
variabel) och ett parameteravsnitt för varje kommando.
För träningskommandot måste CNTK få veta följande:
- läsa data (
reader
avsnitt) - modellfunktionen och dess indata och utdata i beräkningsdiagrammet (
BrainScriptNetworkBuilder
avsnittet) - hyperparametrar för learner (
SGD
avsnitt)
För utvärderingskommandot måste CNTK veta hur testdata (reader
avsnittet) ska läsas.
Följande är konfigurationsfilen som vi börjar med. Som du ser är en CNTK-konfigurationsfil en textfil som består av definitioner av parametrar, som är ordnade i en hierarki med poster. Du kan också se hur CNTK stöder grundläggande parameterersättning med hjälp av syntaxen $parameterName$
. Den faktiska filen innehåller bara några fler parametrar än vad som nämns ovan, men genomsök den och leta reda på de konfigurationsobjekt som just nämnts:
# CNTK Configuration File for creating a slot tagger and an intent tagger.
command = TrainTagger:TestTagger
makeMode = false ; traceLevel = 0 ; deviceId = "auto"
rootDir = "." ; dataDir = "$rootDir$" ; modelDir = "$rootDir$/Models"
modelPath = "$modelDir$/slu.cmf"
vocabSize = 943 ; numLabels = 129 ; numIntents = 26 # number of words in vocab, slot labels, and intent labels
# The command to train the LSTM model
TrainTagger = {
action = "train"
BrainScriptNetworkBuilder = {
inputDim = $vocabSize$
labelDim = $numLabels$
embDim = 150
hiddenDim = 300
model = Sequential (
EmbeddingLayer {embDim} : # embedding
RecurrentLSTMLayer {hiddenDim, goBackwards=false} : # LSTM
DenseLayer {labelDim} # output layer
)
# features
query = Input {inputDim}
slotLabels = Input {labelDim}
# model application
z = model (query)
# loss and metric
ce = CrossEntropyWithSoftmax (slotLabels, z)
errs = ClassificationError (slotLabels, z)
featureNodes = (query)
labelNodes = (slotLabels)
criterionNodes = (ce)
evaluationNodes = (errs)
outputNodes = (z)
}
SGD = {
maxEpochs = 8 ; epochSize = 36000
minibatchSize = 70
learningRatesPerSample = 0.003*2:0.0015*12:0.0003
gradUpdateType = "fsAdaGrad"
gradientClippingWithTruncation = true ; clippingThresholdPerSample = 15.0
firstMBsToShowResult = 10 ; numMBsToShowResult = 100
}
reader = {
readerType = "CNTKTextFormatReader"
file = "$DataDir$/atis.train.ctf"
randomize = true
input = {
query = { alias = "S0" ; dim = $vocabSize$ ; format = "sparse" }
intentLabels = { alias = "S1" ; dim = $numIntents$ ; format = "sparse" }
slotLabels = { alias = "S2" ; dim = $numLabels$ ; format = "sparse" }
}
}
}
# Test the model's accuracy (as an error count)
TestTagger = {
action = "eval"
modelPath = $modelPath$
reader = {
readerType = "CNTKTextFormatReader"
file = "$DataDir$/atis.test.ctf"
randomize = false
input = {
query = { alias = "S0" ; dim = $vocabSize$ ; format = "sparse" }
intentLabels = { alias = "S1" ; dim = $numIntents$ ; format = "sparse" }
slotLabels = { alias = "S2" ; dim = $numLabels$ ; format = "sparse" }
}
}
}
En kort titt på data- och dataläsning
Vi har redan tittat på data.
Men hur genererar du det här formatet?
För att läsa text använder den här självstudien CNTKTextFormatReader
. Den förväntar sig att indata ska ha ett visst format, vilket beskrivs här.
I den här självstudien skapade vi corpora med två steg:
konvertera rådata till en oformaterad textfil som innehåller TAB-avgränsade kolumner med blankstegsavgränsad text. Ett exempel:
BOS show flights from burbank to st. louis on monday EOS (TAB) flight (TAB) O O O O B-fromloc.city_name O B-toloc.city_name I-toloc.city_name O B-depart_date.day_name O
Detta är avsett att vara kompatibelt med kommandots
paste
utdata.konvertera det till CNTK Text Format (CTF) med följande kommando:
python Scripts/txt2ctf.py --map query.wl intent.wl slots.wl --annotated True --input atis.test.txt --output atis.test.ctf
där de tre
.wl
filerna ger vokabulären som oformaterade textfiler, en rad per ord.
I dessa CTFG-filer är våra kolumner märkta S0
, S1
och S2
.
Dessa är anslutna till de faktiska nätverksindata med motsvarande rader i läsardefinitionen:
input = {
query = { alias = "S0" ; dim = $vocabSize$ ; format = "sparse" }
intentLabels = { alias = "S1" ; dim = $numIntents$ ; format = "sparse" }
slotLabels = { alias = "S2" ; dim = $numLabels$ ; format = "sparse" }
}
Köra den
Du hittar konfigurationsfilen ovan under namnet SLUHandsOn.cntk
i arbetsmappen.
Kör den genom att köra konfigurationen ovan med det här kommandot:
cntk configFile=SLUHandsOn.cntk
Detta kommer att köra vår konfiguration, från och med modellträning enligt definitionen i avsnittet som vi gav namnet TrainTagger
.
Efter en något pratsam första loggutdata kommer du snart att se följande:
Training 721479 parameters in 6 parameter tensors.
följt av utdata så här:
Finished Epoch[ 1 of 8]: [Training] ce = 0.77274927 * 36007; errs = 15.344% * 36007
Finished Epoch[ 2 of 8]: [Training] ce = 0.27009664 * 36001; errs = 5.883% * 36001
Finished Epoch[ 3 of 8]: [Training] ce = 0.16390425 * 36005; errs = 3.688% * 36005
Finished Epoch[ 4 of 8]: [Training] ce = 0.13121604 * 35997; errs = 2.761% * 35997
Finished Epoch[ 5 of 8]: [Training] ce = 0.09308497 * 36000; errs = 2.028% * 36000
Finished Epoch[ 6 of 8]: [Training] ce = 0.08537533 * 35999; errs = 1.917% * 35999
Finished Epoch[ 7 of 8]: [Training] ce = 0.07477648 * 35997; errs = 1.686% * 35997
Finished Epoch[ 8 of 8]: [Training] ce = 0.06114417 * 36018; errs = 1.380% * 36018
Detta visar hur inlärningen fortsätter över epoker (passerar genom data).
Efter två epoker har till exempel korsentropy-kriteriet, som vi hade namngett ce
i konfigurationsfilen, nått 0,27 mätt på 36001-exemplen i denna epok och att felfrekvensen är 5,883 % på samma träningsexempel från 36016.
36001 kommer från det faktum att vår konfiguration definierade epokstorleken som 36000. Epokstorleken är antalet exempel – räknas som ordtoken, inte meningar – för att bearbeta mellan modellkontrollpunkter. Eftersom meningarna har varierande längd och inte nödvändigtvis summerar till multiplar med exakt 36 000 ord ser du en liten variant.
När träningen har slutförts (lite mindre än 2 minuter på en Titan-X eller en Surface Book) fortsätter CNTK med EvalTagger
åtgärden
Final Results: Minibatch[1-1]: errs = 2.922% * 10984; ce = 0.14306181 * 10984; perplexity = 1.15380111
På vår testuppsättning har facketiketter förutsagts med en felfrekvens på 2,9 %. Inte alls dåligt, för ett så enkelt system!
På en dator med enbart processor kan den vara 4 eller fler gånger långsammare. För att säkerställa att systemet fortskrider tidigt kan du aktivera spårning för att se partiella resultat, vilket bör visas relativt snabbt:
cntk configFile=SLUHandsOn.cntk traceLevel=1
Epoch[ 1 of 8]-Minibatch[ 1- 1, 0.19%]: ce = 4.86535690 * 67; errs = 100.000% * 67
Epoch[ 1 of 8]-Minibatch[ 2- 2, 0.39%]: ce = 4.83886670 * 63; errs = 57.143% * 63
Epoch[ 1 of 8]-Minibatch[ 3- 3, 0.58%]: ce = 4.78657442 * 68; errs = 36.765% * 68
...
Om du inte vill vänta tills detta har slutförts kan du köra en mellanliggande modell, t.ex.
cntk configFile=SLUHandsOn.cntk command=TestTagger modelPath=Models/slu.cmf.4
Final Results: Minibatch[1-1]: errs = 3.851% * 10984; ce = 0.18932937 * 10984; perplexity = 1.20843890
eller testa vår förtränade modell, som du hittar i arbetsmappen:
cntk configFile=SLUHandsOn.cntk command=TestTagger modelPath=slu.forward.nobn.cmf
Final Results: Minibatch[1-1]: errs = 2.922% * 10984; ce = 0.14306181 * 10984; perplexity = 1.15380111
Ändra modellen
I följande avsnitt får du uppgifter för att öva på att ändra CNTK-konfigurationer. Lösningarna ges i slutet av det här dokumentet... men försök utan!
Ett ord om Sequential()
Innan vi går till aktiviteterna ska vi ta en titt igen på modellen som vi precis körde. Modellen beskrivs i vad vi kallar funktionssammansättningsstil.
model = Sequential (
EmbeddingLayer {embDim} : # embedding
RecurrentLSTMLayer {hiddenDim, goBackwards=false} : # LSTM
DenseLayer {labelDim, initValueScale=7} # output layer
)
där kolonet (:
) är BrainScripts syntax för att uttrycka matriser. Till exempel (F:G:H)
är en matris med tre element, F
, G
och H
.
Du kanske känner till notationen "sekventiell" från andra verktyg för neurala nätverk.
Om inte, Sequential()
är en kraftfull åtgärd som i ett nötskal gör det möjligt att komprimera en mycket vanlig situation i neurala nätverk där indata bearbetas genom att sprida den genom en förlopp av lager.
Sequential()
tar en matris med funktioner som argument och returnerar en ny funktion som anropar funktionen i ordning, varje gång utdata för en skickas till nästa.
Exempel:
FGH = Sequential (F:G:H)
y = FGH (x)
innebär samma sak som
y = H(G(F(x)))
Detta kallas "funktionssammansättning" och är särskilt praktiskt för att uttrycka neurala nätverk, som ofta har den här formen:
+-------+ +-------+ +-------+
x -->| F |-->| G |-->| H |--> y
+-------+ +-------+ +-------+
När vi kommer tillbaka till vår aktuella Sequential
modell säger uttrycket helt enkelt att vår modell har följande formulär:
+-----------+ +----------------+ +------------+
x -->| Embedding |-->| Recurrent LSTM |-->| DenseLayer |--> y
+-----------+ +----------------+ +------------+
Uppgift 1: Lägg till batchnormalisering
Nu vill vi lägga till nya lager i modellen, särskilt batchnormalisering.
Batchnormalisering är en populär teknik för att påskynda konvergensen. Den används ofta för konfigurationer av bildbearbetning, till exempel vårt andra praktiska labb om bildigenkänning. Men kan det fungera för återkommande modeller också?
Så din uppgift är att infoga batchnormaliseringslager före och efter det återkommande LSTM-lagret. Om du har slutfört de praktiska labbuppgifterna om bildbearbetning kanske du kommer ihåg att batchnormaliseringsskiktet har följande formulär:
BatchNormalizationLayer{}
Så gå vidare och ändra konfigurationen och se vad som händer.
Om allt gick som det ska ser du inte bara förbättrad konvergenshastighet (ce
och errs
) jämfört med den tidigare konfigurationen, utan även en bättre felfrekvens på 2,0 % (jämfört med 2,9 %):
Training 722379 parameters in 10 parameter tensors.
Finished Epoch[ 1 of 8]: [Training] ce = 0.29396894 * 36007; errs = 5.621% * 36007
Finished Epoch[ 2 of 8]: [Training] ce = 0.10104186 * 36001; errs = 2.280% * 36001
Finished Epoch[ 3 of 8]: [Training] ce = 0.05012737 * 36005; errs = 1.258% * 36005
Finished Epoch[ 4 of 8]: [Training] ce = 0.04116407 * 35997; errs = 1.108% * 35997
Finished Epoch[ 5 of 8]: [Training] ce = 0.02602344 * 36000; errs = 0.756% * 36000
Finished Epoch[ 6 of 8]: [Training] ce = 0.02234042 * 35999; errs = 0.622% * 35999
Finished Epoch[ 7 of 8]: [Training] ce = 0.01931362 * 35997; errs = 0.667% * 35997
Finished Epoch[ 8 of 8]: [Training] ce = 0.01714253 * 36018; errs = 0.522% * 36018
Final Results: Minibatch[1-1]: errs = 2.039% * 10984; ce = 0.12888706 * 10984; perplexity = 1.13756164
(Om du inte vill vänta tills träningen har slutförts kan du hitta den resulterande modellen under namnet slu.forward.cmf
.)
Se lösningen här.
Uppgift 2: Lägg till en lookahead
Vår återkommande modell lider av ett strukturellt underskott: Eftersom upprepningen går från vänster till höger har beslutet om en facketikett ingen information om kommande ord. Modellen är lite skev. Din uppgift är att ändra modellen så att indata till upprepningen inte bara består av det aktuella ordet, utan även av nästa (lookahead).
Lösningen ska vara i funktionssammansättningsstil. Därför måste du skriva en BrainScript-funktion som gör följande:
- acceptera ett indataargument.
- beräkna det omedelbara "framtida värdet" för dessa indata med hjälp av
FutureValue()
funktionen (använd det här specifika formuläret:FutureValue (0, input, defaultHiddenActivation=0)
); och - sammanfoga de två i en vektor med dubbelt så stor inbäddningsdimension som använder
Splice()
(använd det här formuläret: Splice(x:y)
)
och infoga sedan den här funktionen i Sequence()
mellan inbäddningen och det återkommande lagret.
Om allt går bra visas följande utdata:
Training 902679 parameters in 10 parameter tensors.
Finished Epoch[ 1 of 8]: [Training] ce = 0.30500536 * 36007; errs = 5.904% * 36007
Finished Epoch[ 2 of 8]: [Training] ce = 0.09723847 * 36001; errs = 2.167% * 36001
Finished Epoch[ 3 of 8]: [Training] ce = 0.04082365 * 36005; errs = 1.047% * 36005
Finished Epoch[ 4 of 8]: [Training] ce = 0.03219930 * 35997; errs = 0.867% * 35997
Finished Epoch[ 5 of 8]: [Training] ce = 0.01524993 * 36000; errs = 0.414% * 36000
Finished Epoch[ 6 of 8]: [Training] ce = 0.01367533 * 35999; errs = 0.383% * 35999
Finished Epoch[ 7 of 8]: [Training] ce = 0.00937027 * 35997; errs = 0.278% * 35997
Finished Epoch[ 8 of 8]: [Training] ce = 0.00584430 * 36018; errs = 0.147% * 36018
Final Results: Minibatch[1-1]: errs = 1.839% * 10984; ce = 0.12023170 * 10984; perplexity = 1.12775812
Det här fungerade! Om du vet vad nästa ord är kan facktaggaren minska felfrekvensen från 2,0 % till 1,84 %.
(Om du inte vill vänta tills träningen har slutförts kan du hitta den resulterande modellen under namnet slu.forward.lookahead.cmf
.)
Se lösningen här.
Uppgift 3: Dubbelriktad återkommande modell
Aha, kunskap om framtida ord hjälper. Så i stället för en lookahead med ett ord, varför inte titta framåt förrän hela vägen till slutet av meningen, genom en bakåtsträvande upprepning? Låt oss skapa en dubbelriktad modell!
Din uppgift är att implementera ett nytt lager som utför både en framåt- och en bakåtrekursion över data och sammanfogar utdatavektorerna.
Observera dock att detta skiljer sig från föregående uppgift eftersom det dubbelriktade lagret innehåller inlärbara modellparametrar. I funktionssammansättningsstil är mönstret för att implementera ett lager med modellparametrar att skriva en fabriksfunktion som skapar ett funktionsobjekt.
Ett funktionsobjekt, även kallat functor, är ett objekt som är både en funktion och ett -objekt. Vilket innebär att inget annat än innehåller data kan fortfarande anropas som om det var en funktion.
Är till exempel LinearLayer{outDim}
en fabriksfunktion som returnerar ett funktionsobjekt som innehåller en viktmatris W
, en bias b
och en annan funktion för att beräkna W * input + b
.
Om du till exempel säger LinearLayer{1024}
skapas det här funktionsobjektet, som sedan kan användas som vilken annan funktion som helst, även omedelbart: LinearLayer{1024}(x)
.
Förvirrad? Låt oss ta ett exempel: Låt oss implementera ett nytt lager som kombinerar ett linjärt lager med en efterföljande batchnormalisering. Om du vill tillåta funktionssammansättning måste lagret realiseras som en fabriksfunktion, vilket kan se ut så här:
LinearLayerWithBN {outDim} = {
F = LinearLayer {outDim}
G = BatchNormalization {normalizationTimeConstant=2048}
apply (x) = G(F(x))
}.apply
Om du anropar den här fabriksfunktionen skapas först en post (anges av {...}
) med tre medlemmar: F
, G
och apply
. I det här exemplet F
är och G
själva funktionsobjekten, och apply
är den funktion som ska tillämpas på data.
Att lägga .apply
till i det här uttrycket innebär vad .x
som alltid betyder i BrainScript för att få åtkomst till en postmedlem. Om du till exempel anropar LinearLayerWithBN{1024}
skapas ett objekt som innehåller ett funktionsobjekt i linjärt lager med namnet F
, ett batchnormaliseringsfunktionsobjekt G
och apply
som är den funktion som implementerar den faktiska driften av det här lagret med hjälp av F
och G
. Den returnerar apply
sedan . På utsidan apply()
ser ut och fungerar som en funktion. Under huven kommer dock apply()
att hålla fast vid den post som den tillhör och därmed behålla åtkomsten till sina specifika instanser av F
och G
.
Nu tillbaka till vår uppgift. Nu måste du skapa en fabriksfunktion, ungefär som i exemplet ovan.
Du ska skapa en fabriksfunktion som skapar två återkommande lagerinstanser (en framåt, en bakåt) och sedan definierar en apply (x)
funktion som tillämpar båda lagerinstanserna på samma x
och sammanfogar de två resultaten.
Okej, försök! Om du vill veta hur du realiserar en bakåtrekursion i CNTK kan du ta en ledtråd från hur rekursion framåt görs. Gör också följande:
- ta bort lookahead med ett ord som du lade till i föregående uppgift, som vi vill ersätta. Och
- ändra parametern
hiddenDim
från 300 till 150 för att hålla det totala antalet modellparametrar begränsat.
Om du kör den här modellen skapas följande utdata:
Training 542379 parameters in 13 parameter tensors.
Finished Epoch[ 1 of 8]: [Training] ce = 0.27651655 * 36007; errs = 5.288% * 36007
Finished Epoch[ 2 of 8]: [Training] ce = 0.08179804 * 36001; errs = 1.869% * 36001
Finished Epoch[ 3 of 8]: [Training] ce = 0.03528780 * 36005; errs = 0.828% * 36005
Finished Epoch[ 4 of 8]: [Training] ce = 0.02602517 * 35997; errs = 0.675% * 35997
Finished Epoch[ 5 of 8]: [Training] ce = 0.01310307 * 36000; errs = 0.386% * 36000
Finished Epoch[ 6 of 8]: [Training] ce = 0.01310714 * 35999; errs = 0.358% * 35999
Finished Epoch[ 7 of 8]: [Training] ce = 0.00900459 * 35997; errs = 0.300% * 35997
Finished Epoch[ 8 of 8]: [Training] ce = 0.00589050 * 36018; errs = 0.161% * 36018
Final Results: Minibatch[1-1]: errs = 1.830% * 10984; ce = 0.11924878 * 10984; perplexity = 1.12665017
Fungerar som en charm! Den här modellen uppnår 1,83 %, en liten bit bättre än lookahead-modellen ovan. Den dubbelriktade modellen har 40 % färre parametrar än lookahead-modellen. Men om du går tillbaka och tittar närmare på de fullständiga loggutdata (visas inte på den här webbsidan) kan det hända att lookahead tränade cirka 30 % snabbare. Detta beror på att lookahead-modellen har både mindre vågräta beroenden (ett i stället för två upprepningar) och större matrisprodukter och därmed kan uppnå högre parallellitet.
Se lösningen här.
Uppgift 4: Avsiktsklassificering
Det visar sig att den modell som vi har konstruerat hittills enkelt kan omvandlas till en avsiktsklassificerare.
Kom ihåg att vår datafil innehåller den här ytterligare kolumnen med namnet S1
.
Den här kolumnen innehåller en enskild etikett per mening som anger frågans avsikt att hitta information om ämnen som airport
eller airfare
.
Uppgiften att klassificera en hel sekvens i en enda etikett kallas sekvensklassificering. Vår sekvensklassificerare implementeras som en återkommande LSTM (vi har redan den) där vi tar dess dolda tillstånd för det sista steget. Detta ger oss en enda vektor för varje sekvens. Den här vektorn matas sedan in i ett kompakt lager för softmax-klassificering.
CNTK har en åtgärd för att extrahera det sista tillståndet från en sekvens med namnet BS.Sequences.Last()
.
Den här åtgärden respekterar det faktum att samma minibatch kan innehålla sekvenser med mycket olika längder och att de är ordnade i minnet i ett packat format.
På samma sätt kan vi använda BS.Sequences.First()
för bakåtrekursion.
Din uppgift är att ändra det dubbelriktade nätverket från uppgift 3 så att den sista ramen extraheras från den framåtriktade rekursionen och den första ramen extraheras från bakåtrekursionen och de två vektorerna sammanfogas. Den sammanfogade vektorn (kallas ibland en tankevektor) ska sedan vara indata för det täta skiktet.
Du måste också ändra etiketten från facket till avsiktsetiketten: Byt bara namn på indatavariabeln (slotLabels
) så att den i stället matchar namnet som används i läsaravsnittet för avsiktsetiketterna och matcha även dimensionen.
Försök med ändringen. Om du gör det rätt kommer du dock att konfronteras med ett irriterande felmeddelande, och ett långt felmeddelande på det:
EXCEPTION occurred: Dynamic axis layout '*' is shared between inputs 'intentLabels'
and 'query', but layouts generated from the input data are incompatible on this axis.
Are you using different sequence lengths? Did you consider adding a DynamicAxis()
to the Input nodes?
"Använder du olika sekvenslängder?" Oh ja! Frågan och avsiktsetiketten gör – avsiktsetiketten är bara en enskild token per fråga. Det är en sekvens med 1 element! Så hur man åtgärdar detta?
CNTK tillåter att olika variabler i nätverket har olika sekvenslängder. Du kan betrakta sekvenslängden som ytterligare en symbolisk tensordimension. Variabler med samma längd delar samma symboliska längddimension. Om två variabler har olika längd måste detta deklareras uttryckligen, annars förutsätter CNTK att alla variabler har samma symboliska längd.
Detta görs genom att skapa ett nytt dynamiskt axelobjekt och associera det med någon av indata, enligt följande:
n = DynamicAxis()
query = Input {inputDim, dynamicAxis=n}
CNTK har en standardaxel. Som du kan gissa från undantaget ovan är dess namn '*'.
Därför behöver du bara deklarera en ny axel. den andra indataaxeln (intentLabels
) fortsätter att använda standardaxeln.
Nu bör vi vara bra att köra och se följande utdata:
Training 511376 parameters in 13 parameter tensors.
Finished Epoch[ 1 of 8]: [Training] ce = 1.17365003 * 2702; errs = 21.318% * 2702
Finished Epoch[ 2 of 8]: [Training] ce = 0.40112341 * 2677; errs = 9.189% * 2677
Finished Epoch[ 3 of 8]: [Training] ce = 0.17041608 * 2688; errs = 4.167% * 2688
Finished Epoch[ 4 of 8]: [Training] ce = 0.09521124 * 2702; errs = 2.739% * 2702
Finished Epoch[ 5 of 8]: [Training] ce = 0.08287138 * 2697; errs = 2.262% * 2697
Finished Epoch[ 6 of 8]: [Training] ce = 0.07138554 * 2707; errs = 2.032% * 2707
Finished Epoch[ 7 of 8]: [Training] ce = 0.06220047 * 2677; errs = 1.419% * 2677
Finished Epoch[ 8 of 8]: [Training] ce = 0.05072431 * 2686; errs = 1.340% * 2686
Final Results: Minibatch[1-1]: errs = 4.143% * 893; ce = 0.27832144 * 893; perplexity = 1.32091072
Utan mycket ansträngning har vi uppnått en felfrekvens på 4,1 %. Mycket trevligt för ett första skott (även om inte riktigt state of the art på denna uppgift, som är på 3%).
Du kanske märker en sak dock: Antalet prover per epok är nu runt 2700. Det beror på att det här är antalet etikettexempel, som vi nu bara har en per mening. Vi har ett kraftigt minskat antal övervakningssignaler i den här uppgiften. Detta bör uppmuntra oss att försöka öka minibatchstorleken. Vi provar 256 i stället för 70:
Finished Epoch[ 1 of 8]: [Training] ce = 1.11500325 * 2702; errs = 19.282% * 2702
Finished Epoch[ 2 of 8]: [Training] ce = 0.29961089 * 2677; errs = 6.052% * 2677
Finished Epoch[ 3 of 8]: [Training] ce = 0.09018802 * 2688; errs = 2.418% * 2688
Finished Epoch[ 4 of 8]: [Training] ce = 0.04838102 * 2702; errs = 1.258% * 2702
Finished Epoch[ 5 of 8]: [Training] ce = 0.02996789 * 2697; errs = 0.704% * 2697
Finished Epoch[ 6 of 8]: [Training] ce = 0.02142932 * 2707; errs = 0.517% * 2707
Finished Epoch[ 7 of 8]: [Training] ce = 0.01220149 * 2677; errs = 0.299% * 2677
Finished Epoch[ 8 of 8]: [Training] ce = 0.01312233 * 2686; errs = 0.186% * 2686
Det här systemet lär sig mycket bättre! (Observera dock att den här skillnaden troligen är en artefakt som orsakas av vårt fsAdagrad
toningsnormaliseringsschema och vanligtvis försvinner snart när större datamängder används.)
Den resulterande felfrekvensen är dock högre:
Final Results: Minibatch[1-1]: errs = 4.479% * 893; ce = 0.31638223 * 893; perplexity = 1.37215463
men denna skillnad motsvarar faktiskt 3 fel, vilket inte är betydande.
Se lösningen här.
Uppgift 5: Parallell träning
Om du har flera GPU:er kan du med CNTK parallellisera träningen med hjälp av MPI (Message-Passing Interface). Den här modellen är för liten för att förvänta sig någon snabbhet. parallellisering av en så liten modell underutnyttjer kraftigt tillgängliga GPU:er. Låt oss ändå gå igenom rörelserna, så att du vet hur du gör det när du går vidare till verkliga arbetsbelastningar.
Lägg till följande rader i SGD
blocket:
SGD = {
...
parallelTrain = {
parallelizationMethod = "DataParallelSGD"
parallelizationStartEpoch = 1
distributedMBReading = true
dataParallelSGD = { gradientBits = 2 }
}
}
och kör sedan det här kommandot:
mpiexec -np 4 cntk configFile=SLUHandsOn_Solution4.cntk stderr=Models/log parallelTrain=true command=TrainTagger
Detta kör träningen på 4 GPU:er med 1-bitars SGD-algoritmen (2-bitars SGD i det här fallet faktiskt).
Uppskattningen TestTagger
skadade inte noggrannheten: Felfrekvensen är 4,367 %, två fel fler (kör åtgärden separat på en enda GPU).
Slutsats
Den här självstudien har introducerat funktionssammansättningsstil som ett kompakt sätt att representera nätverk. Många typer av neurala nätverk är lämpliga för att representera dem på det här sättet, vilket är en mer direkt och mindre felbenägen översättning av ett diagram till en nätverksbeskrivning.
Den här självstudien övade på att ta en befintlig konfiguration i funktionssammansättningsstil och ändra den på specifika sätt:
- lägga till ett lager (från vårt galleri med fördefinierade lager)
- definiera och använda en funktion
- definiera och använda en layer factory-funktion
I självstudien diskuterades även hanteringen av flera tidsdimensioner, och vi har sett hur du parallelliserar träningen.
Lösningar
Nedan visas lösningarna på uppgifterna ovan. Inget fusk!
Lösning 1: Lägga till batchnormalisering
Den ändrade modellfunktionen har följande formulär:
model = Sequential (
EmbeddingLayer {embDim} : # embedding
BatchNormalizationLayer {} : ##### added
RecurrentLSTMLayer {hiddenDim, goBackwards=false} : # LSTM
BatchNormalizationLayer {} : ##### added
DenseLayer {labelDim} # output layer
)
Lösning 2: Lägg till en Lookahead
Lookahead-funktionen kan definieras så här:
OneWordLookahead (x) = Splice (x : DelayLayer {T=-1} (x))
och det skulle infogas i modellen så här:
model = Sequential (
EmbeddingLayer {embDim} :
OneWordLookahead : ##### added
BatchNormalizationLayer {} :
RecurrentLSTMLayer {hiddenDim, goBackwards=false} :
BatchNormalizationLayer {} :
DenseLayer {labelDim}
)
Lösning 3: Dubbelriktad återkommande modell
Det dubbelriktade återkommande lagret kan skrivas så här:
BiRecurrentLSTMLayer {outDim} = {
F = RecurrentLSTMLayer {outDim, goBackwards=false}
G = RecurrentLSTMLayer {outDim, goBackwards=true}
apply (x) = Splice (F(x):G(x))
}.apply
och sedan används så här:
hiddenDim = 150 ##### changed from 300 to 150
model = Sequential (
EmbeddingLayer {embDim} :
###OneWordLookahead : ##### removed
BatchNormalizationLayer {} :
BiRecurrentLSTMLayer {hiddenDim} :
BatchNormalizationLayer {} :
DenseLayer {labelDim}
)
Lösning 4: Avsiktsklassificering
Minska sekvenserna till den sista/första dolda i det återkommande lagret:
apply (x) = Splice (BS.Sequences.Last(F(x)):BS.Sequences.First(G(x)))
##### added Last() and First() calls ^^^
Ändra etikettindata från plats till avsikt:
intentDim = $numIntents$ ###### name change
...
DenseLayer {intentDim} ##### different dimension
...
intentLabels = Input {intentDim}
...
ce = CrossEntropyWithSoftmax (intentLabels, z)
errs = ErrorPrediction (intentLabels, z)
...
labelNodes = (intentLabels)
Använd en ny dynamisk axel:
n = DynamicAxis() ##### added
query = Input {inputDim, dynamicAxis=n} ##### use dynamic axis
Bekräftelse
Vi vill tacka Derek Liu för att ha förberett grunden för den här självstudien.