Nuta
Dostęp do tej strony wymaga autoryzacji. Możesz spróbować się zalogować lub zmienić katalog.
Dostęp do tej strony wymaga autoryzacji. Możesz spróbować zmienić katalogi.
Ta sekcja jest specyfikacją wyrażeń BrainScript, chociaż celowo używamy języka nieformalnego, aby zachować czytelność i dostępność. Jego odpowiednik jest specyfikacją składni definicji funkcji BrainScript, którą można znaleźć tutaj.
Każdy skrypt mózgu jest wyrażeniem, które z kolei składa się z wyrażeń przypisanych do zmiennych składowych rekordu. Najbardziej zewnętrznym poziomem opisu sieci jest dorozumiane wyrażenie rekordu. Język BrainScript ma następujące rodzaje wyrażeń:
- literały, takie jak liczby i ciągi
- operacje matematyczne podobne do prefiksu i jednoargumentowego, takie jak
a + b - ternary wyrażenie warunkowe
- wywołania funkcji
- rekordy, dostęp do elementu członkowskiego rekordu
- tablice, dostęp do elementu tablicy
- wyrażenia funkcji (lambdas)
- wbudowana konstrukcja obiektów języka C++
Celowo zachowaliśmy składnię dla każdego z tych języków tak blisko, jak to możliwe do popularnych języków, więc wiele z poniższych informacji będzie wyglądać bardzo znajomo.
Pojęcia
Przed opisem poszczególnych rodzajów wyrażeń najpierw niektóre podstawowe pojęcia.
Bezpośrednie i odroczone obliczenia
BrainScript zna dwa rodzaje wartości: natychmiastowe i odroczone. Wartości natychmiastowe są obliczane podczas przetwarzania kodu BrainScript, a odroczone wartości to obiekty reprezentujące węzły w sieci obliczeniowej. Sieć obliczeniowa opisuje rzeczywiste obliczenia wykonywane przez aparat wykonywania CNTK podczas trenowania i używania modelu.
Natychmiastowe wartości w języku BrainScript mają na celu sparametryzowanie obliczeń. Określają one wymiary tensorowe, liczbę warstw sieciowych, nazwę ścieżki do załadowania modelu z itp. Ponieważ zmienne BrainScript są niezmienne, natychmiastowe wartości są zawsze stałe.
Wartości odroczone wynikają z podstawowego celu skryptów mózgu: do opisania sieci obliczeniowej. Sieć obliczeniowa może być postrzegana jako funkcja przekazywana do procedury trenowania lub wnioskowania, która następnie wykonuje funkcję sieciową za pośrednictwem aparatu wykonywania CNTK. W związku z tym wynik wielu wyrażeń BrainScript jest węzłem obliczeniowym w sieci obliczeniowej, a nie rzeczywistą wartością. Z punktu widzenia języka BrainScript odroczona wartość jest obiektem C++ typu ComputationNode , który reprezentuje węzeł sieciowy. Na przykład biorąc sumę dwóch węzłów sieciowych tworzy nowy węzeł sieciowy, który reprezentuje operację sumowania, która przyjmuje dwa węzły jako dane wejściowe.
Skalarnie a macierze a tensor
Wszystkie wartości w sieci obliczeniowej to tablice liczbowe nwymiarowe, które nazywamy tensorami, a n oznacza rangę tensora. Wymiary tensorowe są jawnie określane dla danych wejściowych i parametrów modelu; i automatycznie wnioskowane przez operatory.
Najbardziej typowym typem danych do obliczeń, macierzy, są tylko tensorami rangi 2. Wektory kolumn są tensorami rangi 1, a wektory wierszy są rangą 2. Produkt macierzy jest wspólną operacją w sieciach neuronowych.
Tensor są zawsze odroczone wartości, czyli obiekty na odroczonym wykresie obliczeniowym. Każda operacja obejmująca macierz lub tensor staje się częścią grafu obliczeniowego i jest obliczana podczas trenowania i wnioskowania. Wymiary tensor są jednak wnioskowane/sprawdzane z góry w czasie przetwarzania BS.
Skalarnie mogą być wartościami natychmiastowymi lub odroczonymi. Skalary, które parametryzują samą sieć obliczeniową, taką jak wymiary tensoru, muszą być natychmiastowe, tj. kompilowalne w momencie przetwarzania kodu BrainScript. Odroczone skalarnie to szereg-1 tensor wymiaru [1]. Są one częścią samej sieci, w tym uczenia się parametrów skalarnych, takich jak samozwańcze i stałe, jak w .Log (Constant (1) + Exp(x))
Dynamiczne wpisywanie
BrainScript to dynamicznie wpisany język z niezwykle prostym systemem typów. Typy są sprawdzane podczas przetwarzania kodu BrainScript, gdy jest używana wartość.
Wartości natychmiastowe to liczba typów, wartość logiczna, ciąg, rekord, tablica, funkcja/lambda lub jedna ze wstępnie zdefiniowanych klas języka C++ CNTK. Ich typy są sprawdzane w czasie użycia (na przykład COND argument instrukcji jest weryfikowany jako Booleanelement , a dostęp do if elementu tablicy wymaga, aby obiekt był tablicą).
Wszystkie wartości odroczone są tensorami. Wymiary tensor są częścią ich typu, które są sprawdzane lub wnioskowane podczas przetwarzania języka BrainScript.
Wyrażenia między bezpośrednim skalarem a odroczonym tensorem muszą jawnie przekonwertować skalar na odroczony Constant(). Na przykład nieliniowość Softplus musi być zapisana jako Log (Constant(1) + Exp (x)). (Planowane jest usunięcie tego wymagania w nadchodzącej aktualizacji).
Typy wyrażeń
Literały
Literały to stałe liczbowe, logiczne lub ciągowe, jak można się spodziewać. Przykłady:
13,42,3.1415926538,1e30true,false"my_model.dnn",'single quotes'
Literały liczbowe są zawsze zmiennoprzecinkowe o podwójnej precyzji. Nie ma jawnego typu liczby całkowitej w języku BrainScript, chociaż niektóre wyrażenia, takie jak indeksy tablicowe, kończą się niepowodzeniem z powodu błędu, jeśli przedstawione wartości nie są liczbami całkowitymi.
Literały ciągu mogą używać cudzysłowów pojedynczych lub podwójnych, ale nie mają możliwości ucieczki cudzysłowów ani innych znaków wewnątrz (ciąg zawierający zarówno cudzysłów pojedynczy, jak i podwójny, muszą być obliczane, np. "He'd say " + '"Yes!" in a jiffy.'). Literały ciągu mogą obejmować wiele wierszy; na przykład:
I3 = Parameter (3, 3, init='fromLiteral', initFromLiteral = '1 0 0
0 1 0
0 0 1')
Operacje przyrostowe i jednoargumentowe
Język BrainScript obsługuje operatory podane poniżej. Operatory BrainScript są wybierane, aby oznaczać, czego można oczekiwać z popularnych języków, z wyjątkiem .* (produktu mądrego elementu), * (produktu macierzy) i specjalnych semantyki emisji operacji mądrych elementami.
Operatory +liczbowe infiksów, -, *, /.*
+,-i*mają zastosowanie do skalarów, macierzy i tensorów..*określa produkt mądry element. Uwaga dla użytkowników języka Python: jest to odpowiednik numpy's*./jest obsługiwana tylko w przypadku skalarnych. Podział mądry elementów można napisać przy użyciu wbudowanychReciprocal(x)obliczeń element-wise1/x.
Operatory przyrostków logicznych &&, ||
Oznaczają one odpowiednio wartości logiczne I i OR.
Łączenie ciągów (+)
Ciągi są łączone za pomocą polecenia +. Przykład: BS.Networks.Load (dir + "/model.dnn").
Operatory porównania
Sześć operatorów porównania to <, ==, >i ich negacje >=, !=, <=. Można je zastosować do wszystkich wartości natychmiastowych zgodnie z oczekiwaniami; ich wynik jest wartością logiczną.
Aby zastosować operatory porównania do tensorów, należy zamiast tego użyć wbudowanych funkcji, takich jak Greater().
Jednoargumentowy-, !
Oznaczają one odpowiednio negację i negację logiczną. ! obecnie można używać tylko dla skalarnych.
Operacje elementowe i semantyka emisji
W przypadku zastosowania do macierzy/tensorów stosowane +-.* są elementy mądre.
Wszystkie operacje mądre obsługują semantyka emisji. Emisja oznacza, że każdy wymiar określony jako 1 będzie automatycznie powtarzany w celu dopasowania do dowolnego wymiaru.
Na przykład wektor wiersza wymiaru [1 x N] można bezpośrednio dodać do macierzy wymiaru [M x N]. Wymiar 1 będzie automatycznie powtarzany M . Ponadto wymiary tensor są automatycznie dopełniane wymiarami 1 . Na przykład można dodać wektor kolumny wymiaru [M][M x N] macierzy. W takim przypadku wymiary wektora kolumny są automatycznie dopełniane w celu [M x 1] dopasowania do rangi macierzy.
Uwaga dla użytkowników języka Python: w przeciwieństwie do narzędzia numpy wymiary emisji są wyrównane do lewej.
Operator macierzy produktu*
Operacja A * B określa produkt macierzy. Można go również zastosować do rozrzednych macierzy, co zwiększa wydajność obsługi wektorów tekstowych lub etykiet reprezentowanych jako wektory jedno-gorące. W CNTK produkt macierzy ma rozszerzoną interpretację, która pozwala na używanie go z tensorami rangi > 2. Można na przykład pomnożyć każdą kolumnę w tensorze rangi 3 indywidualnie za pomocą macierzy.
Produkt macierzy i jego rozszerzenie tensor są szczegółowo opisane tutaj.
Uwaga: Aby pomnożyć za pomocą skalarnego, użyj produktu .*mądrego elementu .
Użytkownicy języka Python powinni pamiętać, że numpy używa * operatora dla produktu mądrego elementu , a nie produktu macierzy. * operator CNTK odpowiada numpyoperatorowi "sdot()", a CNTK odpowiednik operatora języka Python * dla numpy tablic to .*.
Operator warunkowy if
Warunkowe w języku BrainScript to wyrażenia, takie jak operator języka C++ ? . Składnia BrainScript to if COND then TVAL else EVAL, gdzie COND musi być bezpośrednim wyrażeniem logicznym, a wynikiem wyrażenia jest TVAL wartość true COND i EVAL w przeciwnym razie. Wyrażenie if jest przydatne do implementowania wielu podobnych konfiguracji sparametryzowanych flag w tym samym języku BrainScript, a także do rekursji.
(Operator if działa tylko dla natychmiastowych wartości skalarnych. Aby zaimplementować warunkowe dla obiektów odroczonych, użyj wbudowanej funkcji BS.Boolean.If(), która umożliwia wybranie wartości z jednego z dwóch tensorów na podstawie tensoru flagi. Ma on postać If (cond, tval, eval).)
Wywołania funkcji
Język BrainScript ma trzy rodzaje funkcji: wbudowane elementy pierwotne (z implementacjami języka C++), funkcje biblioteki (napisane w języku BrainScript) i zdefiniowane przez użytkownika (BrainScript). Przykłady wbudowanych funkcji to Sigmoid() i MaxPooling(). Funkcje biblioteki i zdefiniowane przez użytkownika są mechanicznie takie same, po prostu zapisywane w różnych plikach źródłowych. Wszystkie rodzaje są wywoływane, podobnie jak w przypadku języków matematycznych i typowych, przy użyciu formularza f (arg1, arg2, ...).
Niektóre funkcje akceptują parametry opcjonalne. Parametry opcjonalne są przekazywane jako nazwane parametry, na przykład f (arg1, arg2, option1=..., option2=...).
Funkcje mogą być wywoływane rekursywnie, na przykład:
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)
Zwróć uwagę, if jak operator jest używany do zakończenia rekursji.
Tworzenie warstw
Funkcje mogą tworzyć całe warstwy lub modele, które są obiektami funkcji, które również zachowują się jak funkcje.
Zgodnie z konwencją funkcja tworząca warstwę z parametrami do nauki używa nawiasów klamrowych { } zamiast nawiasów ( ).
Napotkasz wyrażenia takie jak:
h = DenseLayer {1024} (v)
Tutaj, dwa wywołania są w grze. Pierwszy element DenseLayer{1024}to wywołanie funkcji, które tworzy obiekt funkcji, który z kolei jest stosowany do danych (v).
Ponieważ DenseLayer{} zwraca obiekt funkcji z parametrami umożliwiającymi naukę, używa { } go do oznaczania tego.
Rekordy i Record-Member Access
Wyrażenia rekordów to przypisania otoczone nawiasami klamrowymi. Przykład:
{
x = 13
y = x * factorParameter
f (z) = y + z
}
To wyrażenie definiuje rekord z trzema elementami członkowskimi, x, yi f, gdzie f jest funkcją.
W rekordzie wyrażenia mogą odwoływać się do innych elementów członkowskich rekordu ytylko według ich nazwy, podobnie jak x w przypadku dostępu powyżej w przypisaniu elementu .
W przeciwieństwie do wielu języków wpisy rekordów można jednak zadeklarować w dowolnej kolejności. Np. x można zadeklarować po y. Ma to na celu ułatwienie definicji cyklicznych sieci. Każdy element członkowski rekordu jest dostępny z wyrażenia dowolnego innego elementu członkowskiego rekordu. Różni się to od języka Python. i podobne do języka F#.let rec Odwołania cykliczne są zabronione, z szczególnym wyjątkiem PastValue() operacji i FutureValue() .
Gdy rekordy są zagnieżdżone (wyrażenia rekordów używane wewnątrz innych rekordów), elementy członkowskie rekordów są przeszukiwane przez całą hierarchię otaczających zakresy. W rzeczywistości każde przypisanie zmiennej jest częścią rekordu: Zewnętrzny poziom mózgu jest również dorozumianym rekordem. W powyższym przykładzie factorParameter należy przypisać go jako element członkowski rekordu otaczającego zakresu.
Funkcje przypisane wewnątrz rekordu przechwytują elementy członkowskie rekordów, do których się odwołują. Na przykład f() funkcja przechwytuje element y, który z kolei zależy od x elementu i z zewnętrznego zdefiniowanego factorParameterelementu . Przechwytywanie tych środków oznacza, że f() można przekazać jako lambda do zakresów zewnętrznych, które nie zawierają factorParameter ani nie mają do niego dostępu.
Z zewnątrz dostęp do członków rekordów . jest uzyskiwany przy użyciu operatora . Jeśli na przykład przypisano powyższe wyrażenie rekordu do zmiennej r, r.x zwraca 13wartość . Operator . nie przechodzi przez otaczające zakresy: r.factorParameter niepowodzenie z powodu błędu.
(Należy pamiętać, że do CNTK 1,6, zamiast nawiasów klamrowych { ... }, rekordy używały nawiasów kwadratowych [ ... ]. Jest to nadal dozwolone, ale przestarzałe).
Tablice i dostęp do tablic
Język BrainScript ma jednowymiarowy typ tablicy dla wartości natychmiastowych (nie należy go mylić z tensorami). Tablice są indeksowane przy użyciu polecenia [index]. Tablice wielowymiarowe mogą być emulowane jako tablice tablic.
Tablice z co najmniej 2 elementami można zadeklarować przy użyciu : operatora . Na przykład poniższe deklaruje tablicę 3-wymiarową o nazwie imageDims , która następnie zostaje przekazana do ParameterTensor{} deklaracji tensor parametru rangi 3:
imageDims = (256 : 256 : 3)
inputFilter = ParameterTensor {imageDims}
Istnieje również możliwość deklarowania tablic, których wartości odwołują się do siebie nawzajem. W tym celu należy użyć nieco bardziej zaangażowanej składni przypisania tablicy:
arr[i:i0..i1] = f(i)
który tworzy tablicę o nazwie z arr dolną granicą indeksu i górną granicą i0i1indeksu , i oznaczając zmienną indeksu w wyrażeniuf(i) inicjatora, co z kolei oznacza wartość arr[i]. Wartości tablicy są obliczane w sposób leniwy. Umożliwia to wyrażenie inicjatora dla określonego indeksu i uzyskiwanie dostępu do innych elementów arr[j] tej samej tablicy, o ile nie ma zależności cyklicznej. Można to na przykład użyć do deklarowania stosu warstw sieciowych:
layers[l:1..L] =
if l == 1
then DNNLayer (x, hiddenDim, featDim)
else DNNLayer (layers[l-1], hiddenDim, hiddenDim)
W przeciwieństwie do rekursywnej wersji, którą wprowadziliśmy wcześniej, ta wersja zachowuje dostęp do każdej pojedynczej warstwy, mówiąc layers[i].
Alternatywnie istnieje również składnia array[i0..i1] (i => f(i))wyrażeń , która jest mniej wygodna, ale czasami pomocna. Powyższe kwestie wyglądają następująco:
layers = array[1..L] (l =>
if l == 1
then DNNLayer (x, hiddenDim, featDim)
else DNNLayer (layers[l-1], hiddenDim, hiddenDim)
)
Uwaga: obecnie nie ma możliwości zadeklarowania tablicy 0 elementów. Zostanie to rozwiązane w przyszłej wersji CNTK.
Wyrażenia funkcji i wyrażenia lambda
W języku BrainScript funkcje są wartościami. Nazwaną funkcję można przypisać do zmiennej i przekazać jako argument, np.:
Layer (x, m, n, f) = f (ParameterTensor {(m:n)} * x + ParameterTensor {n})
h = Layer (x, 512, 40, Sigmoid)
gdzie Sigmoid jest przekazywany jako funkcja, która jest używana wewnątrz Layer(). Alternatywnie składnia (x => f(x)) lambda przypominająca język C#umożliwia tworzenie wbudowanych funkcji anonimowych. Na przykład definiuje warstwę sieciową z aktywacją Softplus:
h = Layer (x, 512, 40, (x => Log (Constant(1) + Exp (x)))
Składnia lambda jest obecnie ograniczona do funkcji z pojedynczym parametrem.
Wzorzec warstwy
Powyższy Layer() przykład łączy tworzenie parametrów i aplikację funkcji.
Preferowanym wzorcem jest oddzielenie ich od dwóch kroków:
- tworzenie parametrów i zwraca obiekt funkcji, który przechowuje te parametry
- tworzenie funkcji, która stosuje parametry do danych wejściowych
W szczególności ten ostatni jest również elementem członkowskim obiektu funkcji. Powyższy przykład może zostać przepisany jako:
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
i będzie wywoływany jako:
h = Layer {512, 40, Sigmoid} (x)
Przyczyną tego wzorca jest to, że typowe typy sieci składają się z zastosowania jednej funkcji po drugiej do danych wejściowych, które można łatwiej pisać przy użyciu Sequential() funkcji .
CNTK zawiera bogaty zestaw wstępnie zdefiniowanych warstw, które opisano tutaj.
Konstruowanie wbudowanych obiektów CNTK języka C++
Ostatecznie wszystkie wartości w języku BrainScript są obiektami języka C++. Specjalny operator new BrainScript służy do łączenia się ze źródłowymi obiektami CNTK C++. Ma on postać new TYPE ARGRECORD , w której TYPE jest jednym z zakodowanych wstępnie zdefiniowanych obiektów języka C++ uwidocznionych w języku BrainScript i ARGRECORD jest wyrażeniem rekordu przekazywanym do konstruktora języka C++.
Prawdopodobnie kiedykolwiek zobaczysz ten formularz tylko wtedy, gdy używasz formy nawiasu BrainScriptNetworkBuilder, tj. BrainScriptNetworkBuilder = (new ComputationNetwork { ... }), zgodnie z opisem w tym miejscu.
Ale teraz wiesz, co to znaczy: new ComputationNetwork tworzy nowy obiekt języka C++ typu ComputationNetwork, gdzie { ... } po prostu definiuje rekord przekazywany do konstruktora C++ wewnętrznego ComputationNetwork obiektu C++, który następnie będzie szukać 5 określonych elementów członkowskich featureNodes, labelNodes, criterionNodes, evaluationNodesi outputNodes, jak wyjaśniono tutaj.
Pod maską wszystkie wbudowane funkcje są naprawdę new wyrażeniami, które konstruują obiekty klasy ComputationNodeCNTK C++. Aby uzyskać ilustrację, zobacz, jak Tanh() wbudowany element jest faktycznie zdefiniowany jako tworzenie obiektu języka C++:
Tanh (z, tag=') = new ComputationNode { operation = 'Tanh' ; inputs = z /plus the function args/ }
Semantyka obliczania wyrażeń
Wyrażenia brainScript są oceniane przy pierwszym użyciu. Ponieważ podstawowym celem języka BrainScript jest opisanie sieci, wartość wyrażenia jest często węzłem na grafie obliczeniowym na potrzeby odroczonych obliczeń. Na przykład z kąta BrainScript w W1 * r + b1 powyższym przykładzie wyrażenie "daje w wyniku" obiekt, ComputationNode a nie wartość liczbową, natomiast rzeczywiste wartości liczbowe zostaną obliczone przez aparat wykonywania grafu. Tylko wyrażenia brainscript skalarnych (np. 28*28) są "obliczane" w czasie analizowania języka BrainScript. Wyrażenia, które nigdy nie są używane (np. z powodu warunku), nigdy nie są oceniane (ani sprawdzane pod kątem błędów typu).
Typowe wzorce użycia wyrażeń
Poniżej przedstawiono kilka typowych wzorców używanych z językiem BrainScript.
Przestrzenie nazw dla funkcji
Grupując przypisania funkcji w rekordy, można osiągnąć formę nazewnictwa. Przykład:
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))
Zmienne o zakresie lokalnym
Czasami pożądane jest posiadanie zmiennych w zakresie lokalnym i/lub funkcji dla bardziej złożonych wyrażeń. Można to osiągnąć, ujęcie całego wyrażenia w rekord i natychmiastowe uzyskanie dostępu do jego wartości wynikowej. Przykład:
{ x = 13 ; y = x * x }.y
spowoduje utworzenie rekordu "tymczasowego" z elementem członkowskim y , który jest natychmiast odczytywany. Ten rekord jest "tymczasowy", ponieważ nie jest przypisany do zmiennej, dlatego jej elementy członkowskie nie są dostępne z wyjątkiem y.
Ten wzorzec jest często używany do tworzenia warstw NN z wbudowanymi parametrami bardziej czytelnymi, na przykład:
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 W tym miejscu można uznać wartość "zwracaną" tej funkcji.
Dalej: Dowiedz się więcej o definiowaniu funkcji BrainScript
NDLNetworkBuilder (przestarzałe)
Wcześniejsze wersje CNTK używały przestarzałego NDLNetworkBuilder elementu zamiast BrainScriptNetworkBuilder. NDLNetworkBuilder zaimplementowano znacznie zmniejszoną wersję języka BrainScript. Miały następujące ograniczenia:
- Brak składni prefiksu. Wszystkie operatory muszą być wywoływane za pomocą wywołań funkcji. Np.
Plus (Times (W1, r), b1)zamiastW1 * r + b1. - Brak zagnieżdżonych wyrażeń rekordów. Istnieje tylko jeden domniemany rekord zewnętrzny.
- Nie ma wywołania funkcji warunkowej ani wyrażenia cyklicznego.
- Funkcje zdefiniowane przez użytkownika muszą być zadeklarowane w specjalnych
loadblokach i nie mogą być zagnieżdżone. - Ostatnie przypisanie rekordu jest automatycznie używane jako wartość funkcji.
- Wersja
NDLNetworkBuilderjęzyka nie jest kompletna.
NDLNetworkBuilder nie należy już używać.