Forma matricial de retropropagación con normalización de lotes

12

La normalización por lotes se ha acreditado con mejoras sustanciales de rendimiento en redes neuronales profundas. Un montón de material en Internet muestra cómo implementarlo en una activación por activación. Ya implementé backprop usando álgebra matricial, y dado que estoy trabajando en lenguajes de alto nivel (mientras confío en Rcpp(y eventualmente GPU) para la multiplicación de matriz densa), arrancar todo y recurrir a for-loops probablemente ralentizaría mi código sustancialmente, además de ser un gran dolor.

La función de normalización por lotes es

b(xp)=γ(xpμxp)σxp1+β
donde
  • xp es elp th nodo, antes de que se active
  • γ yβ son parámetros escalares
  • μxp yσxp son la media y la DE dexp . (Tenga en cuenta que la raíz cuadrada de la varianza más un factor de fudge se usa normalmente; supongamos que los elementos no son cero para la compacidad)

En forma de matriz, la normalización de lotes para una capa completa sería donde

b(X)=(γ1p)(XμX)σX1+(β1p)
  • es N × pXN×p
  • es un vector columna de unos1N
  • y β son ahora fila p- vectores de los parámetros de normalización por capaγβp
  • y σ X sonmatrices N × p , donde cada columna es unvector N de medias en columna y desviaciones estándarμXσXN×pN
  • es el producto Kronecker y es el producto elementwise (Hadamard)

Una red neuronal de una capa muy simple sin normalización por lotes y un resultado continuo es

y=a(XΓ1)Γ2+ϵ

dónde

  • es p 1 × p 2Γ1p1×p2
  • es p 2 × 1Γ2p2×1
  • es la función de activacióna(.)

Si la pérdida es , a continuación, los gradientes son RR=N1(yy^)2

RΓ1=2VTϵ^RΓ2=XT(a(XΓ1)2ϵ^Γ2T)

dónde

  • V=a(XΓ1)
  • ϵ^=yy^

Bajo la normalización de lotes, la red se convierte en o y

y=a(b(XΓ1))Γ2
y=a((γ1N)(XΓ1μXΓ1)σXΓ11+(β1N))Γ2
No tengo idea de cómo calcular los derivados de los productos Hadamard y Kronecker. Sobre el tema de los productos Kronecker, la literatura se vuelve bastante misteriosa.

¿Existe una forma práctica de calcular , R /β y R /Γ 1 dentro del marco de la matriz? ¿Una expresión simple, sin recurrir a la computación nodo por nodo?R/γR/βR/Γ1

Actualización 1:

He descubierto - más o menos. Es: 1 T N ( un ' ( X Γ 1 ) - 2 varepsilon Γ T 2 ) Algunos R código demuestra que esto es equivalente a la forma de bucle para hacerlo. Primero configure los datos falsos:R/β

1NT(a(XΓ1)2ϵ^Γ2T)
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

Luego calcule derivados:

# 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

Ellos coinciden. Pero todavía estoy confundido, porque realmente no sé por qué esto funciona. Las notas de MatCalc a las que hace referencia @Mark L. Stone dicen que la derivada de debe serβ1N

ABA=(InqTmp)(Invec(B)Im)
mnpqABT
# 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

γΓ1β1

Actualización 2

Leyendo libros de texto, estoy bastante seguro de que R/Γ1R/γvec()R/Γ1wXΓ1Γ1w(γ1)σXΓ11

wXwX

(AB)=AB+AB

y de esto , eso

vec(wXΓ1)vec(Γ1)T=vec(XΓ1)Ivec(w)vec(Γ1)T+vec(w)Ivec(XΓ1)vec(Γ1)T

Actualización 3

Progresando aquí. Me desperté a las 2 de la madrugada con esta idea. Las matemáticas no son buenas para dormir.

R/Γ1

  • w(γ1)σXΓ11
  • "stub"a(b(XΓ1))2ϵ^Γ2T

RΓ1=wXΓ1Γ1("stub")
ijI
RΓij=(wiXi)T("stub"j)
RΓij=(IwiXi)T("stub"j)
RΓij=XiTIwi("stub"j)
RΓ=XT("stub"w)

Y, de hecho, es:

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

Actualización 4

R/γ

  • XΓ~(XΓμXΓ)σXΓ1
  • γ~γ1N

Rγ~=γ~XΓ~γ~("stub")
Rγ~i=(XΓ~)iTIγ~i("stub"i)
Rγ~=(XΓ~)T("stub"γ~)

Más o menos coincide:

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

γ

Parece que he respondido a mi propia pregunta, pero no estoy seguro de si estoy en lo correcto. En este punto, aceptaré una respuesta que prueba rigurosamente (o refuta) lo que he pirateado juntos.

while(not_answered){
  print("Bueller?")
  Sys.sleep(1)
}
genérico_usuario
fuente
2
La sección 14 del capítulo 9 de "Cálculo diferencial de matriz con aplicaciones en estadística y econometría" por Magnus y Neudecker, 3a edición janmagnus.nl/misc/mdc2007-3rdedition cubre los diferenciales de los productos Kronecker y concluye con un ejercicio sobre el diferencial del producto Hadamard. "Notes on Matrix Calculus" de Paul L. Fackler www4.ncsu.edu/~pfackler/MatCalc.pdf tiene mucho material sobre la diferenciación de los productos Kronceker
Mark L. Stone
Gracias por las referencias. He encontrado esas notas de MatCalc antes, pero no cubre Hadamard, y de todos modos nunca estoy seguro de si una regla del cálculo no matricial se aplica o no a un caso matricial. Reglas del producto, reglas de la cadena, etc. Examinaré el libro. Acepto una respuesta que me señala todos los ingredientes que necesito para
dibujarlo
¿Por qué estás haciendo esto? ¿Por qué no utilizar marcos como Keras / TensorFlow? Implementar estos algoritmos de bajo nivel es una pérdida de tiempo productivo, que podría usar para resolver problemas reales
Aksakal
1
Más precisamente, estoy adaptando redes que explotan la estructura paramétrica conocida, tanto en términos de representaciones lineales en los parámetros de datos de entrada, como en la estructura longitudinal / de panel. Los marcos establecidos están tan optimizados que están más allá de mi capacidad de piratear / modificar. Además, las matemáticas son útiles en general. Muchos codemonkeys no tienen idea de lo que están haciendo. Asimismo, aprender lo suficiente como Rcpppara implementarlo de manera eficiente es útil.
generic_user
1
@ MarkL.Stone no solo es teóricamente sólido, ¡es prácticamente fácil! ¡Un proceso más o menos mecánico! PS
generic_user

Respuestas:

1

No es una respuesta completa, pero para demostrar lo que sugerí en mi comentario si 2 , ... )

b(X)=(XeNμXT)ΓΣX1/2+eNβT
Γ=diag(γ)ΣX1/2=diag(σX11,σX21,)eN
βR=[2ϵ^(Γ2TI)JX(a)(IeN)]T
2ϵ^(Γ2TI)=vec(2ϵ^Γ2T)TJX(a)=diag(vec(a(b(XΓ1))))
βR=(IeNT)vec(a(b(XΓ1))2ϵ^Γ2T)=eNT(a(b(XΓ1))2ϵ^Γ2T)
vec(AXB)=(BTA)vec(X)
γR=[2ϵ^(Γ2TI)JX(a)(ΣXΓ11/2(XΓ1eNμXΓ1T))K]T=KTvec((XΓ1eNμXΓ1T)TWΣXΓ11/2)=diag((XΓ1eNμXΓ1T)TWΣXΓ11/2)
W=a(b(XΓ1))2ϵ^Γ2TKNp×pdΓij=0bγiγiΓ1wΣXμXX
deasmhumnha
fuente