【问题标题】:Parallelising gradient calculation in JuliaJulia 中的并行梯度计算
【发布时间】:2015-10-17 20:39:44
【问题描述】:

前段时间我被说服放弃了我舒适的 matlab 编程并开始使用 Julia 编程。我一直在研究神经网络,我认为现在有了 Julia,我可以通过并行计算梯度来更快地完成任务。

不需要一次性在整个数据集上计算梯度;相反,可以拆分计算。例如,通过将数据集分成几部分,我们可以计算每个部分的部分梯度。然后通过将部分梯度相加来计算总梯度。

虽然原理很简单,但当我与 Julia 并行时,性能会下降,即一个进程比两个进程快!我显然做错了什么......我已经咨询了论坛中提出的其他问题,但我仍然无法拼凑出答案。我认为我的问题在于有很多不必要的数据在移动,但我无法正确修复它。

为了避免发布混乱的神经网络代码,我在下面发布一个更简单的示例,该示例复制了我在线性回归设置中的问题。

下面的代码块为线性回归问题创建了一些数据。代码解释了常量,但 X 是包含数据输入的矩阵。我们随机创建一个权重向量 w,当它与 X 相乘时会创建一些目标 Y

######################################
## CREATE LINEAR REGRESSION PROBLEM ##
######################################

# This code implements a simple linear regression problem

MAXITER = 100   # number of iterations for simple gradient descent
N = 10000       # number of data items
D = 50          # dimension of data items
X = randn(N, D) # create random matrix of data, data items appear row-wise
Wtrue = randn(D,1) # create arbitrary weight matrix to generate targets
Y = X*Wtrue     # generate targets

下面的下一个代码块定义了用于测量回归适应度(即负对数似然)和权重向量梯度的函数w:

####################################
##       DEFINE FUNCTIONS         ##
####################################

@everywhere  begin

  #-------------------------------------------------------------------
  function negative_loglikelihood(Y,X,W)
  #-------------------------------------------------------------------

    # number of data items
    N  = size(X,1)
    # accumulate here log-likelihood
    ll = 0
    for nn=1:N
      ll = ll - 0.5*sum((Y[nn,:] - X[nn,:]*W).^2)
    end

    return ll
  end


  #-------------------------------------------------------------------
  function negative_loglikelihood_grad(Y,X,W, first_index,last_index)
  #-------------------------------------------------------------------

    # number of data items
    N  = size(X,1)
    # accumulate here gradient contributions by each data item
    grad = zeros(similar(W))
    for nn=first_index:last_index
      grad = grad +  X[nn,:]' * (Y[nn,:] - X[nn,:]*W)
    end

    return grad
  end


end

请注意,上述函数是故意不向量化的!我选择不向量化,因为最终的代码(神经网络案例)也不会接受任何向量化(让我们不要对此进行更多详细说明)。

最后,下面的代码块显示了一个非常简单的梯度下降,它试图从给定数据 YXw /strong>:

####################################
##     SOLVE LINEAR REGRESSION    ##
####################################


# start from random initial solution
W = randn(D,1)

# learning rate, set here to some arbitrary small constant
eta = 0.000001

# the following for-loop implements simple gradient descent
for iter=1:MAXITER

  # get gradient
  ref_array = Array(RemoteRef, nworkers())

  # let each worker process part of matrix X
  for index=1:length(workers())

    # first index of subset of X that worker should work on
    first_index       = (index-1)*int(ceil(N/nworkers())) + 1
    # last index of subset of X that worker should work on
    last_index        = min((index)*(int(ceil(N/nworkers()))), N)

    ref_array[index] = @spawn negative_loglikelihood_grad(Y,X,W, first_index,last_index)
  end

  # gather the gradients calculated on parts of matrix X
  grad = zeros(similar(W))
  for index=1:length(workers())
    grad = grad + fetch(ref_array[index])
  end

  # now that we have the gradient we can update parameters W
  W = W + eta*grad;

  # report progress, monitor optimisation
  @printf("Iter %d neg_loglikel=%.4f\n",iter, negative_loglikelihood(Y,X,W))
end

正如我们希望看到的那样,我在这里尝试以最简单的方式并行计算梯度。我的策略是尽可能多地打破可用工人的梯度计算。每个工人只需要在矩阵 X 的一部分上工作,这部分由 first_indexlast_index 指定。因此,每个工人都应该使用X[first_index:last_index,:]。例如,对于 4 个工人和 N = 10000,工作应该被划分如下:

  • worker 1 => first_index = 1, last_index = 2500
  • worker 2 => first_index = 2501, last_index = 5000
  • worker 3 => first_index = 5001, last_index = 7500
  • worker 4 => first_index = 7501, last_index = 10000

不幸的是,如果我只有一个工人,整个代码的运行速度会更快。如果通过addprocs() 添加更多worker,代码运行速度会变慢。可以通过创建更多数据项来加剧这个问题,例如使用 N=20000。 数据项越多,降级越明显。 在我的具有 N=20000 和一个核心的特定计算环境中,代码运行时间约为 9 秒。 N=20000 和 4 个核心大约需要 18 秒!

我尝试了许多不同的方法,灵感来自这个论坛中的问题和答案,但不幸的是无济于事。我意识到并行化是幼稚的,数据移动一定是问题所在,但我不知道如何正确地做到这一点。似乎关于这个问题的文档也有点稀缺(就像 Ivo Balbaert 的好书一样)。

感谢您的帮助,因为我已经为此困扰了很长一段时间,我的工作确实需要它。任何想运行代码的人,为了省去复制粘贴的麻烦,您可以获取代码here

感谢您抽出宝贵时间阅读这个冗长的问题!帮我把它变成一个模范答案,任何 Julia 的新手都可以参考!

【问题讨论】:

    标签: parallel-processing gradient julia linear-regression


    【解决方案1】:

    我会说 GD 不适合使用任何建议的方法进行并行化:SharedArrayDistributedArray,或者自己实现数据块的分布。

    问题不在于 Julia,而在于 GD 算法。 考虑代码:

    主要流程:

    for iter = 1:iterations #iterations: "the more the better"
        δ = _gradient_descent_shared(X, y, θ) 
        θ = θ -  α * (δ/N)   
    end
    

    问题出在上面的for循环中,这是必须的。不管_gradient_descent_shared再好,总的迭代次数扼杀了并行化的崇高概念。

    阅读问题和上述建议后,我开始使用SharedArray 实施 GD。请注意,我不是 SharedArrays 领域的专家。

    主要流程部分(简单实现无需正则化):

    run_gradient_descent(X::SharedArray, y::SharedArray, θ::SharedArray, α, iterations) = begin
      N = length(y)
    
      for iter = 1:iterations 
        δ = _gradient_descent_shared(X, y, θ) 
        θ = θ -  α * (δ/N)   
      end     
      θ
    end
    
    _gradient_descent_shared(X::SharedArray, y::SharedArray, θ::SharedArray, op=(+)) = begin            
        if size(X,1) <= length(procs(X))
            return _gradient_descent_serial(X, y, θ)
        else
            rrefs = map(p -> (@spawnat p _gradient_descent_serial(X, y, θ)), procs(X))
            return mapreduce(r -> fetch(r), op, rrefs)
        end 
    end
    

    所有工人通用的代码:

    #= Returns the range of indices of a chunk for every worker on which it can work. 
    The function splits data examples (N rows into chunks), 
    not the parts of the particular example (features dimensionality remains intact).=# 
    @everywhere function _worker_range(S::SharedArray)
        idx = indexpids(S)
        if idx == 0
            return 1:size(S,1), 1:size(S,2)
        end
        nchunks = length(procs(S))
        splits = [round(Int, s) for s in linspace(0,size(S,1),nchunks+1)]
        splits[idx]+1:splits[idx+1], 1:size(S,2)
    end
    
    #Computations on the chunk of the all data.
    @everywhere _gradient_descent_serial(X::SharedArray, y::SharedArray, θ::SharedArray)  = begin
        prange = _worker_range(X)
    
        pX = sdata(X[prange[1], prange[2]])
        py = sdata(y[prange[1],:])
    
        tempδ = pX' * (pX * sdata(θ) .- py)         
    end
    

    数据加载和训练。让我假设我们有:

    • X::Array 中的特征,大小为 (N,D),其中 N - 示例数,特征的 D 维数
    • y::Array 中的标签,大小为 (N,1)

    主要代码可能如下所示:

    X=[ones(size(X,1)) X] #adding the artificial coordinate 
    N, D = size(X)
    MAXITER = 500
    α = 0.01
    
    initialθ = SharedArray(Float64, (D,1))
    sX = convert(SharedArray, X)
    sy = convert(SharedArray, y)  
    X = nothing
    y = nothing
    gc() 
    
    finalθ = run_gradient_descent(sX, sy, initialθ, α, MAXITER);
    

    在实现这个并运行(在我的 Intell Clore i7 的 8 核上)之后,我在我的训练多类(19 类)训练数据(串行 GD 为 715 秒 /共享 GD 为 665 秒)。

    如果我的实现是正确的(请检查一下 - 我指望着那个),那么 GD 算法的并行化就不值得了。当然,在 1 核上使用随机 GD 可能会获得更好的加速效果。

    【讨论】:

    • 您好 Maciek,非常感谢您的帖子。我仍然被这个问题折磨着。我必须承认,我不明白为什么加速 GD 会出现问题。我的意思是,一旦数据块被分发或访问(我不知道设计的特定选择有多重要)到不需要交互的不同工作人员,应该会看到改进(至少比你和我所拥有的更多)达到)。当然,分发数据需要时间,但从长远来看,这个时间长度可以忽略不计吗?我会尽快检查您的实施情况。干杯。
    • 恕我直言,由于减少的数量(for-loop),我们不会加快 GD。每次迭代都必须在主流程中得到确认。我已经用大小为 N = 3000,D = 6000 的训练集对其进行了测试。也许如果 N 更大,它会加速得更好。如果您有足够的数据,请检查一下。
    • 您好 Maciek,感谢您回来。我明白你所说的“每次迭代都必须在主流程中得到确认”的意思,我开始明白这一点。我不知道如何从这里开始,但我非常感谢您的反馈。谢谢。
    • 我终于接受了上面的答案。虽然它不能完全解决我的问题,但它很有帮助,而且肯定是我得到的最彻底的回应。
    【解决方案2】:

    如果您想减少数据移动量,您应该强烈考虑使用 SharedArrays。您可以只预分配一个输出向量,并将其作为参数传递给每个工作人员。正如你所建议的,每个工人都设置了一大块。

    【讨论】:

    • 感谢您的回答!您是否介意详细说明如何实际使用 SharedArrays,或者指出一个易于理解的示例?例如,我什至无法弄清楚如何在上面的示例代码中从现有的数据矩阵 X 中创建一个 SharedArray,或者将我的数据直接加载到一个 SharedArray 中。 Julia 文档显示了构造函数是什么,但不幸的是,这对我没有帮助。干杯。
    • 我现在在另一篇文章中看到,您的回答建议从现有数组中创建一个 SharedArray,如下所示:sharedx = convert(SharedArray,x)。确实很有用。
    • 我正在尝试使用 SharedArray 类型,但我不确定如何使用它,以便我将原始矩阵 X 的块分配给工人。我想我需要做的不仅仅是 sharedX = convert(SharedArray,X) 然后只是传递 sharedX ,或者?
    猜你喜欢
    • 1970-01-01
    • 2015-09-09
    • 2020-05-13
    • 1970-01-01
    • 2015-10-18
    • 1970-01-01
    • 1970-01-01
    • 2012-08-18
    • 2013-07-27
    相关资源
    最近更新 更多