A normalização de lotes foi creditada com melhorias substanciais de desempenho em redes neurais profundas. Muito material na internet mostra como implementá-lo, ativação por ativação. Eu já implementei backprop usando álgebra matricial e, como estou trabalhando em linguagens de alto nível (enquanto confio em Rcpp
(e eventualmente GPUs) para multiplicação densa de matrizes), rasgar tudo e recorrer a for
loops provavelmente atrasaria meu código substancialmente, além de ser uma dor enorme.
A função de normalização do lote é
- é o nó th, antes que ele é ativado
- e são parâmetros escalares
- e são a média e o DP de . (Observe que a raiz quadrada da variação mais um fator de correção é normalmente usado - vamos assumir elementos diferentes de zero para compactação)
Na forma de matriz, a normalização de lote para uma camada inteira seria
- é
- é um vetor de coluna de unidades
- e agora sãovetores delinha dos parâmetros de normalização por camada
- e sãomatrizes , em que cada coluna é umvetor de médias decolunae desvios padrão
- é o produto Kronecker e é o produto elementwise (Hadamard)
Uma rede neural de uma camada muito simples, sem normalização por lotes e um resultado contínuo é
Onde
- é
- é
- é a função de ativação
Se a perda é , em seguida, os gradientes são
Onde
Sob normalização do lote, a rede se torna
Existe uma maneira prática de computação , ∂ R / ∂ beta , e ∂ R / ∂ y- 1 no âmbito da matriz? Uma expressão simples, sem recorrer à computação nó por nó?
Atualização 1:
Eu descobri - mais ou menos. É: 1 T N ( um ' ( X Γ 1 ) ⊙ - 2 £ Γ T 2 ) Algumas R código demonstra que este é equivalente ao modo looping para fazê-lo. Primeiro, configure os dados falsos:
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
Em seguida, calcule derivativos:
# 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
Eles combinam. Mas ainda estou confuso, porque realmente não sei por que isso funciona. As notas do MatCalc referenciadas por @ Mark L. Stone dizem que a derivada de deve ser
, onde os subscritosm,n, ep,qsão as dimensões deumeB. Té a matriz de comutação, que é apenas 1 aqui porque ambas as entradas são vetores. Eu tento isso e obtenho um resultado que não parece útil:
# 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
Isso não é conformável. Claramente, não estou entendendo essas regras derivadas da Kronecker. Ajudar com isso seria ótimo. Ainda estou totalmente preso aos outros derivativos, para e Γ 1 - esses são mais difíceis porque não entram de maneira aditiva como o β ⊗ 1 .
Atualização 2
Lendo livros didáticos, tenho certeza de que e ∂ R / ∂ γ exigirão o uso do operador. Mas, aparentemente, sou incapaz de seguir suficientemente as derivações para poder traduzi-las em código. Por exemplo, ∂ R / ∂ Γ 1 envolverá a derivada de w ⊙ X Γ 1 em relação a Γ 1 , onde w ≡ ( γ ⊗ 1 ) ⊙ σ - 1vec()
(que podemos tratar como uma matriz constante no momento).
Meu instinto é simplesmente dizer "a resposta é ", mas que, obviamente, não funciona porque w não está de acordo com X .
Eu sei que
e a partir disso , que
Atualização 3
Fazendo progresso aqui. Eu acordei às 2 da manhã na noite passada com essa ideia. A matemática não é boa para dormir.
And, in fact it is:
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
Update 4
Here, I think, is . First
Similar to before, the chain rule gets you as far as
It sort of matches:
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
The diagonal on the first is the same as the vector on the second. But really since the derivative is with respect to a matrix -- albeit one with a certain structure, the output should be a similar matrix with the same structure. Should I take the diagonal of the matrix approach and simply take it to be ? I'm not sure.
It seems that I have answered my own question but I am unsure whether I am correct. At this point I will accept an answer that rigorously proves (or disproves) what I've sort of hacked together.
while(not_answered){
print("Bueller?")
Sys.sleep(1)
}
Rcpp
to implement it efficiently is useful.