Normalizacji partii przypisano znaczną poprawę wydajności w głębokich sieciach neuronowych. Wiele materiałów w Internecie pokazuje, jak wdrożyć je na zasadzie aktywacja po aktywacji. Zaimplementowałem już backprop za pomocą algebry macierzy i biorąc pod uwagę, że pracuję w językach wysokiego poziomu (polegając na Rcpp
(i ewentualnie GPU) na gęstym mnożeniu macierzy), zgrywanie wszystkiego i uciekanie się do for
pętli prawdopodobnie spowolniłoby mój kod zasadniczo, oprócz tego, że jest ogromnym bólem.
Funkcja normalizacji partii to gdzie
- p jest tym węzłem, zanim zostanie aktywowany
- β i są parametrami skalarnymi
- σ x p x p i są średnią i SD . (Zauważ, że zwykle stosowany jest pierwiastek kwadratowy wariancji plus współczynnik krówki - załóżmy, że elementy są niezerowe).
W postaci macierzowej normalizacja partii dla całej warstwy to gdzie
- N × p to
- jest wektorem kolumnowym jedności
- β p i są teraz wierszami wektorów parametrów normalizacji dla poszczególnych warstw
- σ X N × p N i są macierzami , gdzie każda kolumna jest wektorem wektorów średnich i odchyleń standardowych
- to produkt Kronecker, a to produkt elementowy (Hadamard)
Bardzo prosta jednowarstwowa sieć neuronowa bez normalizacji partii i ciągłym wynikiem jest
gdzie
- to
- p 2 × 1 to
- to funkcja aktywacji
Jeśli utrata wynosi , wówczas gradienty to ∂ R
gdzie
W ramach normalizacji partii siatka staje się lub Nie mam pojęcia, jak obliczyć pochodne produktów Hadamarda i Kroneckera. Literatura na temat produktów Kronecker jest dość tajemnicza. y = a ( ( γ ⊗ 1 N
Czy istnieje praktyczny sposób obliczania , i w ramach matrycy? Proste wyrażenie bez uciekania się do obliczeń węzeł-węzeł?∂ R / ∂ β ∂ R / ∂ Γ 1
Aktualizacja 1:
Odkryłem - w pewnym sensie. Jest to: Niektóre kody R pokazują, że jest to równoważne z zapętleniem. Najpierw skonfiguruj fałszywe dane:
set.seed(1)
library(dplyr)
library(foreach)
#numbers of obs, variables, and hidden layers
N <- 10
p1 <- 7
p2 <- 4
a <- function (v) {
v[v < 0] <- 0
v
}
ap <- function (v) {
v[v < 0] <- 0
v[v >= 0] <- 1
v
}
# parameters
G1 <- matrix(rnorm(p1*p2), nrow = p1)
G2 <- rnorm(p2)
gamma <- 1:p2+1
beta <- (1:p2+1)*-1
# error
u <- rnorm(10)
# matrix batch norm function
b <- function(x, bet = beta, gam = gamma){
xs <- scale(x)
gk <- t(matrix(gam)) %x% matrix(rep(1, N))
bk <- t(matrix(bet)) %x% matrix(rep(1, N))
gk*xs+bk
}
# activation-wise batch norm function
bi <- function(x, i){
xs <- scale(x)
gk <- t(matrix(gamma[i]))
bk <- t(matrix(beta[i]))
suppressWarnings(gk*xs[,i]+bk)
}
X <- round(runif(N*p1, -5, 5)) %>% matrix(nrow = N)
# the neural net
y <- a(b(X %*% G1)) %*% G2 + u
Następnie oblicz pochodne:
# drdbeta -- the matrix way
drdb <- matrix(rep(1, N*1), nrow = 1) %*% (-2*u %*% t(G2) * ap(b(X%*%G1)))
drdb
[,1] [,2] [,3] [,4]
[1,] -0.4460901 0.3899186 1.26758 -0.09589582
# the looping way
foreach(i = 1:4, .combine = c) %do%{
sum(-2*u*matrix(ap(bi(X[,i, drop = FALSE]%*%G1[i,], i)))*G2[i])
}
[1] -0.44609015 0.38991862 1.26758024 -0.09589582
Pasują do siebie. Ale nadal jestem zdezorientowany, ponieważ tak naprawdę nie wiem, dlaczego to działa. Notatki MatCalc, do których odwołuje się @ Mark L. Stone, mówią, że pochodna powinna być
# playing with the kroneker derivative rule
A <- t(matrix(beta))
B <- matrix(rep(1, N))
diag(rep(1, ncol(A) *ncol(B))) %*% diag(rep(1, ncol(A))) %x% (B) %x% diag(nrow(A))
[,1] [,2] [,3] [,4]
[1,] 1 0 0 0
[2,] 1 0 0 0
snip
[13,] 0 1 0 0
[14,] 0 1 0 0
snip
[28,] 0 0 1 0
[29,] 0 0 1 0
[snip
[39,] 0 0 0 1
[40,] 0 0 0 1
To nie jest zgodne. Najwyraźniej nie rozumiem tych reguł pochodnych Kroneckera. Pomoc z nimi byłaby świetna. Nadal całkowicie utknąłem w innych pochodnych, dla i - są one trudniejsze, ponieważ nie wchodzą w addytywne, jak .
Aktualizacja 2
Czytając podręczniki, jestem całkiem pewien, że i będą wymagały użycia operatora. Ale najwyraźniej nie jestem w stanie śledzić pochodnych w stopniu wystarczającym, aby móc je przetłumaczyć na kod. Na przykład, będzie wzięcia pochodnej w odniesieniu do , gdzie (które w tej chwili możemy traktować jako stałą macierz). ∂ R / ∂ γvec()
Instynktownie mówię „odpowiedź brzmi ”, ale to oczywiście nie działa, ponieważ nie jest zgodne z .
Wiem, że
i z tego , że
Aktualizacja 3
Robię postępy tutaj. Obudziłem się wczoraj o 2 nad ranem z tym pomysłem. Matematyka nie jest dobra na sen.
Oto , po cukru notacyjnego:
Oto, co masz po do końca reguły łańcucha: Zacznij od zrobienia tego w pętli - i subskrybują kolumny, a jest zgodną matrycą tożsamości:
I w rzeczywistości jest to:
stub <- (-2*u %*% t(G2) * ap(b(X%*%G1)))
w <- t(matrix(gamma)) %x% matrix(rep(1, N)) * (apply(X%*%G1, 2, sd) %>% t %x% matrix(rep(1, N)))
drdG1 <- t(X) %*% (stub*w)
loop_drdG1 <- drdG1*NA
for (i in 1:7){
for (j in 1:4){
loop_drdG1[i,j] <- t(X[,i]) %*% diag(w[,j]) %*% (stub[,j])
}
}
> loop_drdG1
[,1] [,2] [,3] [,4]
[1,] -61.531877 122.66157 360.08132 -51.666215
[2,] 7.047767 -14.04947 -41.24316 5.917769
[3,] 124.157678 -247.50384 -726.56422 104.250961
[4,] 44.151682 -88.01478 -258.37333 37.072659
[5,] 22.478082 -44.80924 -131.54056 18.874078
[6,] 22.098857 -44.05327 -129.32135 18.555655
[7,] 79.617345 -158.71430 -465.91653 66.851965
> drdG1
[,1] [,2] [,3] [,4]
[1,] -61.531877 122.66157 360.08132 -51.666215
[2,] 7.047767 -14.04947 -41.24316 5.917769
[3,] 124.157678 -247.50384 -726.56422 104.250961
[4,] 44.151682 -88.01478 -258.37333 37.072659
[5,] 22.478082 -44.80924 -131.54056 18.874078
[6,] 22.098857 -44.05327 -129.32135 18.555655
[7,] 79.617345 -158.71430 -465.91653 66.851965
Aktualizacja 4
Myślę, że tutaj jest . Pierwszy
Podobnie jak poprzednio, reguła łańcuchowa prowadzi cię do Pętla daje Co, podobnie jak poprzednio, jest w zasadzie wstępnym pomnożeniem kodu pośredniczącego. Powinien zatem być równoważny z:
To rodzaj dopasowań:
drdg <- t(scale(X %*% G1)) %*% (stub * t(matrix(gamma)) %x% matrix(rep(1, N)))
loop_drdg <- foreach(i = 1:4, .combine = c) %do% {
t(scale(X %*% G1)[,i]) %*% (stub[,i, drop = F] * gamma[i])
}
> drdg
[,1] [,2] [,3] [,4]
[1,] 0.8580574 -1.125017 -4.876398 0.4611406
[2,] -4.5463304 5.960787 25.837103 -2.4433071
[3,] 2.0706860 -2.714919 -11.767849 1.1128364
[4,] -8.5641868 11.228681 48.670853 -4.6025996
> loop_drdg
[1] 0.8580574 5.9607870 -11.7678486 -4.6025996
Przekątna na pierwszym jest taka sama jak wektor na drugim. Ale tak naprawdę, ponieważ pochodna dotyczy macierzy - aczkolwiek o określonej strukturze, wynik powinien być podobną macierzą o tej samej strukturze. Czy powinienem wziąć przekątną matrycy i po prostu przyjąć, że jest to ? Nie jestem pewny.
Wygląda na to, że odpowiedziałem na własne pytanie, ale nie jestem pewien, czy mam rację. W tym momencie zaakceptuję odpowiedź, która rygorystycznie udowodni (lub obali) to, co razem zhackowałem.
while(not_answered){
print("Bueller?")
Sys.sleep(1)
}
Rcpp
Przydatna jest również nauka wystarczająca do skutecznego wdrożenia.