当前位置: 首页 > 知识库问答 >
问题:

MacOS Python在训练神经网络时比Julia更快

申阳伯
2023-03-14

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

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

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

这里有我遗漏的东西吗?

编辑:我知道我的Julia代码在优化方面还有很多地方需要改进,我非常感谢任何让它更快的技巧。然而,这不是朱莉娅在我的笔记本电脑上变慢的情况,而是Python要快得多。在我的桌面上,Python运行一个纪元大约13秒,在笔记本电脑上只需要大约4.4s。我最感兴趣的是这种差异来自哪里。我知道这个问题可能措辞不当。

笔记本电脑上的版本:

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)
Python 2.7.14 (default, Mar 22 2018, 14:43:05) 
[GCC 4.2.1 Compatible Apple LLVM 9.0.0 (clang-900.0.39.2)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> import numpy
>>> numpy.show_config()
lapack_opt_info:
    extra_link_args = ['-Wl,-framework', '-Wl,Accelerate']
    extra_compile_args = ['-msse3']
    define_macros = [('NO_ATLAS_INFO', 3), ('HAVE_CBLAS', None)]
openblas_lapack_info:
  NOT AVAILABLE
atlas_3_10_blas_threads_info:
  NOT AVAILABLE
atlas_threads_info:
  NOT AVAILABLE
openblas_clapack_info:
  NOT AVAILABLE
atlas_3_10_threads_info:
  NOT AVAILABLE
atlas_blas_info:
  NOT AVAILABLE
atlas_3_10_blas_info:
  NOT AVAILABLE
atlas_blas_threads_info:
  NOT AVAILABLE
openblas_info:
  NOT AVAILABLE
blas_mkl_info:
  NOT AVAILABLE
blas_opt_info:
    extra_link_args = ['-Wl,-framework', '-Wl,Accelerate']
    extra_compile_args = ['-msse3', '-I/System/Library/Frameworks/vecLib.framework/Headers']
    define_macros = [('NO_ATLAS_INFO', 3), ('HAVE_CBLAS', None)]
blis_info:
  NOT AVAILABLE
atlas_info:
  NOT AVAILABLE
atlas_3_10_info:
  NOT AVAILABLE
lapack_mkl_info:
  NOT AVAILABLE

Julia代码(顺序):

using MLDatasets

mutable struct network
    num_layers::Int64
    sizearr::Array{Int64,1}
    biases::Array{Array{Float64,1},1}
    weights::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])]

    network(num_layers, sizearr, biases, weights)
end

σ(z) = 1/(1+e^(-z))
σ_prime(z) = σ(z)*(1-σ(z))

function (net::network)(a)
    for (w, b) in zip(net.weights, net.biases)
        a = σ.(w*a + b)
    end
    return a
end

function SGDtrain(net::network, training_data, epochs, mini_batch_size, η, test_data=nothing)
    n_test = test_data != nothing ? length(test_data):nothing
    n = length(training_data)

    for j in 1:epochs
        training_data = shuffle(training_data)
        mini_batches = [training_data[k:k+mini_batch_size-1] for k in 1:mini_batch_size:n]

        @time for batch in mini_batches
            update_batch(net, batch, η)
        end

        if test_data != nothing
            println("Epoch ", j,": ", evaluate(net, test_data), "/", n_test)
        else
            println("Epoch ", j," complete.")
        end
    end
end

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

    for (x, y) in batch
        δ_∇_b, δ_∇_w = backprop(net, x, y)
        ∇_b += δ_∇_b
        ∇_w += δ_∇_w
    end

    net.biases -= (η/length(batch))∇_b
    net.weights -= (η/length(batch))∇_w
end

function backprop(net::network, x, y)
    ∇_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)

    for i in 1:len-1
        b = net.biases[i]; w = net.weights[i]
        z = w*activation .+ b
        zs[i] = z
        activation = σ.(z)
        activations[i+1] = activation[:]
    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

function evaluate(net::network, test_data)
    test_results = [(findmax(net(x))[2] - 1, y) for (x, y) in test_data]
    return sum(Int(x == y) for (x, y) in test_results)
end

function loaddata(rng = 1:50000)
    train_x, train_y = MNIST.traindata(Float64, Vector(rng))
    train_x = [train_x[:,:,x][:] for x in 1:size(train_x, 3)]
    train_y = [vectorize(x) for x in train_y]
    traindata = [(x, y) for (x, y) in zip(train_x, train_y)]

    test_x, test_y = MNIST.testdata(Float64)
    test_x = [test_x[:,:,x][:] for x in 1:size(test_x, 3)]
    testdata = [(x, y) for (x, y) in zip(test_x, test_y)]
    return traindata, testdata
end

function vectorize(n)
    ev = zeros(10,1)
    ev[n+1] = 1
    return ev
end

function main()
    net = network([784, 30, 10])
    traindata, testdata = loaddata()
    SGDtrain(net, traindata, 10, 10, 1.25, testdata)
end

共有1个答案

颜功
2023-03-14

我开始运行你的代码:

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)

事实上,您正在使用复制意味着您可能应该预分配东西!所以让我们从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 。此函数是一个矩阵乘法,它将数据写入数组Ca\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

但是我不使用分配的',而是使用reshape,因为我只想要一个视图:

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,则可以使用ProfileView替换第二部分。jl。我得到:

所以大部分时间都花在BLAS上,但是有一个问题。看到像_w = δ_∇_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_mul_B!调用,因此现在几乎所有的事情都受到我的OpenBLAS速度的限制,所以我完成了。我可以做的一些额外的事情是多线程一些其他的循环,但是给分析的回报会很小,所以我会把它留给你(基本上只是把Threads.@threads放在循环上,比如_w[i] . = δ_∇_w[i])。

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

 类似资料:
  • 我有一个关于卷积神经网络()训练的问题。 我成功地使用tensorflow训练了一个网络,它获取一个输入图像(1600像素),然后输出三个匹配的类中的一个。 使用不同的培训课程测试网络,效果良好。然而当我给它一个不同的第四个图像(不包含任何经过训练的3个图像)时,它总是返回一个随机匹配到其中一个类。 我的问题是,如何训练网络来分类图像不属于这三个训练图像中的任何一个?类似的例子是,如果我针对mni

  • 在本章中,我们将了解可以使用TensorFlow框架实现的神经网络训练的各个方面。 以下几个建议,可以评估 - 1. 反向传播 反向传播是计算偏导数的简单方法,其中包括最适合神经网络的基本形式的合成。 2. 随机梯度下降 在随机梯度下降中,批处理是示例的总数,用户用于在单次迭代中计算梯度。到目前为止,假设批处理已经是整个数据集。最好的例子是谷歌规模; 数据集通常包含数十亿甚至数千亿个示例。 3.

  • 第 10 章介绍了人工神经网络,并训练了第一个深度神经网络。 但它非常浅,只有两个隐藏层。 如果你需要解决非常复杂的问题,例如检测高分辨率图像中的数百种类型的对象,该怎么办? 你可能需要训练更深的 DNN,也许有 10 层或更多,每层包含数百个神经元,通过数十万个连接相连。 这可不像公园散步那么简单,可能碰到下面这些问题: 你将面临棘手的梯度消失问题(或相关的梯度爆炸问题):在反向传播过程中,梯度

  • 第 10 章介绍了人工神经网络,并训练了我们的第一个深度神经网络。 但它是一个非常浅的 DNN,只有两个隐藏层。 如果你需要解决非常复杂的问题,例如检测高分辨率图像中的数百种类型的对象,该怎么办? 你可能需要训练更深的 DNN,也许有 10 层,每层包含数百个神经元,通过数十万个连接来连接。 这不会是闲庭信步: 首先,你将面临棘手的梯度消失问题(或相关的梯度爆炸问题),这会影响深度神经网络,并使较

  • 我们现在将学习如何训练神经网络。 我们还将学习Python深度学习中的反向传播算法和反向传递。 我们必须找到神经网络权重的最佳值,以获得所需的输出。 为了训练神经网络,我们使用迭代梯度下降法。 我们最初从权重的随机初始化开始。 在随机初始化之后,我们使用前向传播过程对数据的某个子集进行预测,计算相应的成本函数C,并将每个权重w更新为与dC/dw成比例的量,即成本函数的导数。重量。 比例常数称为学习

  • 我知道前馈神经网络的基本知识,以及如何使用反向传播算法对其进行训练,但我正在寻找一种算法,以便使用强化学习在线训练神经网络。 例如,我想用人工神经网络解决手推车杆摆动问题。在这种情况下,我不知道应该怎么控制钟摆,我只知道我离理想位置有多近。我需要让安在奖惩的基础上学习。因此,监督学习不是一种选择。 另一种情况类似于蛇游戏,反馈被延迟,并且仅限于进球和反进球,而不是奖励。 我可以为第一种情况想出一些