macOS Python的numpy训练神经网络的速度比Julia快

macOS Python的numpy训练神经网络的速度比Julia快,python,numpy,optimization,julia,Python,Numpy,Optimization,Julia,我尝试移植提交给Julia的NN代码,希望能加快网络训练的速度。在我的桌面上,事实证明是这样的 然而,在我的MacBook上,Python+numpy比Julia强很多。 使用相同的参数进行训练,Python的速度是Julia的两倍多(一个历元的速度是4.4s对10.6s)。考虑到Julia在我的桌面上比Python快(大约2秒),似乎有一些Python/numpy在mac上使用的资源Julia没有。即使是并行化代码,也只能使我降低到~6.6s(尽管这可能是因为我没有编写并行代码的经验)。我认为

我尝试移植提交给Julia的NN代码,希望能加快网络训练的速度。在我的桌面上,事实证明是这样的

然而,在我的MacBook上,Python+numpy比Julia强很多。
使用相同的参数进行训练,Python的速度是Julia的两倍多(一个历元的速度是4.4s对10.6s)。考虑到Julia在我的桌面上比Python快(大约2秒),似乎有一些Python/numpy在mac上使用的资源Julia没有。即使是并行化代码,也只能使我降低到~6.6s(尽管这可能是因为我没有编写并行代码的经验)。我认为问题可能在于Julia的BLAS比mac中本机使用的vecLib库慢,但尝试不同的构建似乎并没有让我更接近。我尝试了使用USE_SYSTEM_BLAS=1构建和使用MKL构建,其中MKL给出了更快的结果(上面发布的时间)

我将在下面发布我的笔记本电脑版本信息以及我的Julia实现,以供参考。我当时没有访问桌面的权限,但我在Windows上运行的是同一版本的Julia,使用的是openBLAS,而Python 2.7的干净安装也使用的是openBLAS

这里有我遗漏的东西吗

编辑:我知道我的Julia代码在优化方面还有很多需要改进的地方,我真的很感激任何加快它的技巧。然而,这并不是Julia在我的笔记本电脑上的速度慢,而是Python的速度快得多。在我的台式机上,Python在13秒内运行一个时代,在笔记本电脑上只需4.4秒。我最感兴趣的是这种差异从何而来。我意识到这个问题可能有点措词不当

笔记本电脑上的版本:

julia> versioninfo()
Julia Version 0.6.2
Commit d386e40c17 (2017-12-13 18:08 UTC)
Platform Info:
  OS: macOS (x86_64-apple-darwin17.4.0)
  CPU: Intel(R) Core(TM) i5-7360U CPU @ 2.30GHz
  WORD_SIZE: 64
  BLAS: libmkl_rt
  LAPACK: libmkl_rt
  LIBM: libopenlibm
  LLVM: libLLVM-3.9.1 (ORCJIT, broadwell)

Julia代码(顺序):


我从运行您的代码开始:

7.110379 seconds (1.37 M allocations: 20.570 GiB, 19.81%gc time)
Epoch 1: 7960/10000
6.147297 seconds (1.27 M allocations: 20.566 GiB, 18.33%gc time)
哎哟,每个时代分配21GiB?这是你的问题。它经常影响垃圾收集,而且你的计算机内存越少,它需要的内存就越多。让我们来解决这个问题

其主要思想是预先分配缓冲区,然后修改数组,而不是创建新的数组。在您的代码中,您可以从以下内容开始
backprop

∇_b = copy(net.biases)
∇_w = copy(net.weights)

len = length(net.sizearr)
activation = x
activations = Array{Array{Float64,1}}(len)
activations[1] = x
zs = copy(net.biases)
您正在使用
copy
这一事实意味着您可能应该预先分配东西!让我们从
zs
激活开始。我扩展了您的网络以容纳这些缓存阵列:

mutable struct network
    num_layers::Int64
    sizearr::Array{Int64,1}
    biases::Array{Array{Float64,1},1}
    weights::Array{Array{Float64,2},1}
    zs::Array{Array{Float64,1},1}
    activations::Array{Array{Float64,1},1}
end

function network(sizes)
    num_layers = length(sizes)
    sizearr = sizes
    biases = [randn(y) for y in sizes[2:end]]
    weights = [randn(y, x) for (x, y) in zip(sizes[1:end-1], sizes[2:end])]
    zs = [randn(y) for y in sizes[2:end]]
    activations = [randn(y) for y in sizes[1:end]]
    network(num_layers, sizearr, biases, weights, zs, activations)
end
然后我更改了您的
backprop
以使用这些缓存:

function backprop(net::network, x, y)
    ∇_b = copy(net.biases)
    ∇_w = copy(net.weights)

    len = length(net.sizearr)
    activations = net.activations
    activations[1] .= x
    zs = net.zs

    for i in 1:len-1
        b = net.biases[i]; w = net.weights[i];
        z = zs[i]; activation = activations[i+1]
        z .= w*activations[i] .+ b
        activation .= σ.(z)
    end

    δ = (activations[end] - y) .* σ_prime.(zs[end])
    ∇_b[end] = δ[:]
    ∇_w[end] = δ*activations[end-1]'

    for l in 1:net.num_layers-2
        z = zs[end-l]
        δ = net.weights[end-l+1]'δ .* σ_prime.(z)
        ∇_b[end-l] = δ[:]
        ∇_w[end-l] = δ*activations[end-l-1]'
    end
    return (∇_b, ∇_w)
end
这导致分配的内存大幅减少。但是还有很多事情要做。首先,让我们将
*
更改为
a\u mul\B。此函数是一个矩阵乘法,它将数据写入数组
C
a\u mul\u B!(C,a,B)
),而不是创建新的矩阵,这可以大大减少内存分配。所以我做了:

for l in 1:net.num_layers-2
    z = zs[end-l]
    δ = net.weights[end-l+1]'δ .* σ_prime.(z)
    ∇_b[end-l] .= vec(δ)
    atransp = activations[end-l-1]'
    A_mul_B!(∇_w[end-l],δ,atransp)
end
但是,我没有使用分配的
,而是使用
重塑
,因为我只想要一个视图:

for l in 1:net.num_layers-2
    z = zs[end-l]
    δ = net.weights[end-l+1]'δ .* σ_prime.(z)
    ∇_b[end-l] .= vec(δ)
    atransp = reshape(activations[end-l-1],1,length(activations[end-l-1]))
    A_mul_B!(∇_w[end-l],δ,atransp)
end
(同时,它会更快地发送OpenBLAS。这可能与MKL有所不同)。但你还是在跟我学

    ∇_b = copy(net.biases)
    ∇_w = copy(net.weights)
每一步分配一组δs,所以我所做的下一个更改预先分配了这些δs,并将其全部到位(看起来就像前面的更改一样)

然后我做了一些分析。在朱诺,这只是:

@profile main()
Juno.profiler()
或者,如果您不使用Juno,您可以将第二部分替换为。我得到:

所以大部分时间都花在BLAS上,但有一个问题。查看类似
∇_w+=δ_∇_我们正在创建一组矩阵!相反,我们希望循环并通过每个矩阵的变化矩阵就地更新每个矩阵。这扩展为:

function update_batch(net::network, batch, η)
    ∇_b = net.∇_b
    ∇_w = net.∇_w

    for i in 1:length(∇_b)
      fill!(∇_b[i],0.0)
    end

    for i in 1:length(∇_w)
      fill!(∇_w[i],0.0)
    end

    for (x, y) in batch
        δ_∇_b, δ_∇_w = backprop(net, x, y)
        ∇_b .+= δ_∇_b
        for i in 1:length(∇_w)
          ∇_w[i] .+= δ_∇_w[i]
        end
    end

    for i in 1:length(∇_b)
      net.biases[i] .-= (η/length(batch)).*∇_b[i]
    end

    for i in 1:length(∇_w)
      net.weights[i] .-= (η/length(batch)).*∇_w[i]
    end
end
我按照同样的思路做了一些修改,最终代码如下:

mutable struct network
    num_layers::Int64
    sizearr::Array{Int64,1}
    biases::Array{Array{Float64,1},1}
    weights::Array{Array{Float64,2},1}
    weights_transp::Array{Array{Float64,2},1}
    zs::Array{Array{Float64,1},1}
    activations::Array{Array{Float64,1},1}
    ∇_b::Array{Array{Float64,1},1}
    ∇_w::Array{Array{Float64,2},1}
    δ_∇_b::Array{Array{Float64,1},1}
    δ_∇_w::Array{Array{Float64,2},1}
    δs::Array{Array{Float64,2},1}
end

function network(sizes)
    num_layers = length(sizes)
    sizearr = sizes
    biases = [randn(y) for y in sizes[2:end]]
    weights = [randn(y, x) for (x, y) in zip(sizes[1:end-1], sizes[2:end])]
    weights_transp = [randn(x, y) for (x, y) in zip(sizes[1:end-1], sizes[2:end])]
    zs = [randn(y) for y in sizes[2:end]]
    activations = [randn(y) for y in sizes[1:end]]
    ∇_b = [zeros(y) for y in sizes[2:end]]
    ∇_w = [zeros(y, x) for (x, y) in zip(sizes[1:end-1], sizes[2:end])]
    δ_∇_b = [zeros(y) for y in sizes[2:end]]
    δ_∇_w = [zeros(y, x) for (x, y) in zip(sizes[1:end-1], sizes[2:end])]
    δs = [zeros(y,1) for y in sizes[2:end]]
    network(num_layers, sizearr, biases, weights, weights_transp, zs, activations,∇_b,∇_w,δ_∇_b,δ_∇_w,δs)
end

function update_batch(net::network, batch, η)
    ∇_b = net.∇_b
    ∇_w = net.∇_w

    for i in 1:length(∇_b)
      ∇_b[i] .= 0.0
    end

    for i in 1:length(∇_w)
      ∇_w[i] .= 0.0
    end

    δ_∇_b = net.δ_∇_b
    δ_∇_w = net.δ_∇_w

    for (x, y) in batch
        backprop!(net, x, y)
        for i in 1:length(∇_b)
          ∇_b[i] .+= δ_∇_b[i]
        end
        for i in 1:length(∇_w)
          ∇_w[i] .+= δ_∇_w[i]
        end
    end

    for i in 1:length(∇_b)
      net.biases[i] .-= (η/length(batch)).*∇_b[i]
    end

    for i in 1:length(∇_w)
      net.weights[i] .-= (η/length(batch)).*∇_w[i]
    end
end

function backprop!(net::network, x, y)
    ∇_b = net.δ_∇_b
    ∇_w = net.δ_∇_w

    len = length(net.sizearr)
    activations = net.activations
    activations[1] .= x
    zs = net.zs
    δs = net.δs

    for i in 1:len-1
        b = net.biases[i]; w = net.weights[i];
        z = zs[i]; activation = activations[i+1]
        A_mul_B!(z,w,activations[i])
        z .+= b
        activation .= σ.(z)
    end

    δ = δs[end]
    δ .= (activations[end] .- y) .* σ_prime.(zs[end])
    ∇_b[end] .= vec(δ)
    atransp = reshape(activations[end-1],1,length(activations[end-1]))
    A_mul_B!(∇_w[end],δ,atransp)

    for l in 1:net.num_layers-2
        z = zs[end-l]
        transpose!(net.weights_transp[end-l+1],net.weights[end-l+1])
        A_mul_B!(δs[end-l],net.weights_transp[end-l+1],δ)
        δ = δs[end-l]
        δ .*= σ_prime.(z)
        ∇_b[end-l] .= vec(δ)
        atransp = reshape(activations[end-l-1],1,length(activations[end-l-1]))
        A_mul_B!(∇_w[end-l],δ,atransp)
    end
    return nothing
end
其他一切都没有改变。为了确保完成,我将
@time
添加到
backprop
调用中,并获取:

0.000070 seconds (8 allocations: 352 bytes)
0.000066 seconds (8 allocations: 352 bytes)
0.000090 seconds (8 allocations: 352 bytes)
所以这是不分配的。我将
@time
添加到批处理的
for(x,y)
循环中并获取

0.000636秒(80次分配:3.438千磅) 0.000610秒(80次分配:3.438千磅) 0.000624秒(80次分配:3.438千磅)

因此,这告诉我,基本上所有剩余的分配都来自迭代器(这可以改进,但可能不会改进计时)。因此,最后的时机是:

Epoch 2: 8428/10000
  4.005540 seconds (586.87 k allocations: 23.925 MiB)
Epoch 1: 8858/10000
  3.488674 seconds (414.49 k allocations: 17.082 MiB)
Epoch 2: 9104/10000
这在我的机器上几乎快了2倍,但每个循环的内存分配要少1200倍。这意味着,在RAM较慢和更小的机器上,这种方法应该会更好(我的桌面有相当多的内存,所以它真的不在乎太多!)

最终的配置文件显示大部分时间都在
A\u mul\B调用,所以现在几乎所有的事情都受到我的OpenBLAS速度的限制,所以我完成了。我可以做的一些额外的事情是多线程处理一些其他的循环,但是给分析带来的回报将很小,所以我将把它留给您(基本上就是把
线程放在
之类的循环上。@Threads
∇_w[i].+=δ_∇_w[i]


希望这不仅能改进您的代码,还能教会您如何分析、预分配、使用就地操作以及考虑性能。

您编写的Julia代码非常(有意地)低效。示例:
z=w*activation.+b
这种代码在不需要数组时创建数组。为什么不使用一个缓存阵列来进行非分配呢?然后,数组的数组应该是数组的静态向量(这是一个很小的区别)。另外,您是使用
@btime
计时还是包括Julia的启动+JIT时间?如果您正在使用
main
函数并从命令行调用它,那么可能有一半的时间只是启动Julia+LLVM,而不是实际运行脚本。这不是一种推荐的运行Julia的方法。我基于python实现链接的Julia代码,我意识到这显然不会产生
0.000070 seconds (8 allocations: 352 bytes)
0.000066 seconds (8 allocations: 352 bytes)
0.000090 seconds (8 allocations: 352 bytes)
Epoch 2: 8428/10000
  4.005540 seconds (586.87 k allocations: 23.925 MiB)
Epoch 1: 8858/10000
  3.488674 seconds (414.49 k allocations: 17.082 MiB)
Epoch 2: 9104/10000