批归一化已被认为可在深度神经网络中显着提高性能。互联网上的大量资料显示了如何在逐个激活的基础上实施它。我已经使用矩阵代数实现了backprop,并且考虑到我正在使用高级语言(同时依赖Rcpp
(最终是GPU的)密集矩阵乘法),将所有内容剔除并采用for
-loops可能会使我的代码变慢除了遭受巨大的痛苦之外
批处理归一化函数为 其中
- p是激活之前的个节点
- β和是标量参数
- σ X p X p和是均值和SD的。(请注意,通常使用方差的平方根加上一个模糊系数-假设非零元素为紧凑起见)
以矩阵形式,整个层的批量归一化将为 其中
- 是
- 是1的列向量
- β p和现在是每层归一化参数的行向量
- σ X Ñ × p Ñ和是矩阵,其中每一列都是列均值和标准差的向量
- ⊙是Kronecker产品,是elementwise(Hadamard)产品
,这是一个非常简单的没有批次归一化且连续结果的单层神经网络
哪里
- p 1 × p 2是
- p 2 × 1是
- 是激活函数
如果损失为,则渐变为 ∂ ř
哪里
在批量归一化下,网络变为 或 我不知道如何计算Hadamard和Kronecker产品的导数。关于克罗内克产品,文献变得相当神秘。 Ý = 一个(( γ ⊗ 1 Ñ ) ⊙ (X Γ 1 - μ X Γ 1) ⊙ σ - 1 X Γ 1 + ( β ⊗ 1 Ñ )) Γ 2
是否存在在矩阵框架内计算,和的?一个简单的表达式,无需借助逐节点计算?∂ - [R / ∂ β ∂ - [R / ∂ Γ 1
更新1:
我已经弄清楚了类的。它是: 一些R代码演示了这等效于执行此操作的循环方法。首先设置假数据:1 Ť Ñ(一个'(X Γ 1)⊙ - 2 ε Γ Ť 2)
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
然后计算导数:
# 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
他们匹配。但是我仍然很困惑,因为我真的不知道为什么会这样。@Mark L. Stone引用的MatCalc注释说的派生应为
米Ñpq甲乙Ť
# 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
这是不符合要求的。显然,我不了解那些Kronecker导数规则。那些方面的帮助会很棒。对于和,我仍然完全停留在其他派生类上,因为它们不会像那样累加地输入,所以它们更难。
更新2
在阅读教科书时,我相当确定和将需要使用运算符。但是,我显然无法充分遵循派生方法,无法将其转换为代码。例如,将要涉及利用该衍生物的相对于,其中(目前我们可以将其视为恒定矩阵)。 vec()
我的直觉是简单地说“答案是 ”,但这显然行不通,因为与不符。
我知道
从这个,那个
更新3
在这里取得进展。我昨晚凌晨2点醒来时就想到了这个主意。数学对睡眠不利。
这是,在一些符号糖之后:
这是链规则的结尾,内容如下: 以这种循环方式开始和将对列进行下标,而是一致的身份矩阵:
而且,实际上是:
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
更新4
我认为这里是。第一
与之前类似,链式规则将您带到 循环为您提供 与以前一样,它基本上是预乘存根。因此,它应等效于:
有点匹配:
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
第一个的对角线与第二个的对角线相同。但是实际上,由于导数是关于矩阵的-尽管矩阵具有某种结构,所以输出应该是具有相同结构的相似矩阵。我是否应该采用矩阵方法的对角线并将其简单地设为?我不确定。
看来我已经回答了自己的问题, 但不确定自己是否正确。在这一点上,我将接受一个可以严格证明(或反对)我被黑的东西的答案。
while(not_answered){
print("Bueller?")
Sys.sleep(1)
}
Rcpp
以有效地实施它也是有用的。