Julia

时间:2015-07-27 15:15:32

标签: parallel-processing gradient julia linear-regression

前段时间我被说服放弃了我舒适的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

请注意,上述功能是故意不进行矢量化的!我选择不进行矢量化,因为最终的代码(神经网络案例)也不会允许任何矢量化(让我们不要深入了解这个)。

最后,下面的代码块显示了一个非常简单的梯度下降,试图从给定数据 Y X <恢复参数权重向量 w /强>:

####################################
##     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_index last_index 指定。因此,每个工作人员都应该使用X[first_index:last_index,:]。例如,对于4名工人和N = 10000,工作应划分如下:

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

不幸的是,如果我只有一个工作者,那么整个代码的工作速度会更快。如果通过addprocs()添加更多工作人员,则代码运行速度会变慢。可以通过创建更多数据项来加剧此问题,例如使用 N = 20000 。 随着更多的数据项,降级甚至更加明显。 在我的特定计算环境中, N = 20000 和一个核心,代码在~9秒内运行。 N = 20000 和4个核心需要大约18秒!

我在这个论坛的问题和答案的启发下尝试了许多不同的事情,但遗憾的是无济于事。我意识到并行化是天真的,数据移动一定是问题,但我不知道如何正确地做到这一点。看来文档在这个问题上也有点稀缺(正如Ivo Balbaert的好书)。

我很感激你的帮助,因为我已经坚持了很长一段时间,我真的需要它来完成我的工作。对于任何想要运行代码的人来说,为了省去复制粘贴的麻烦,你可以获得代码here

感谢您花时间阅读这个非常冗长的问题!帮助我把它变成一个模型答案,任何Julia的新人都可以咨询!

2 个答案:

答案 0 :(得分:4)

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

答案 1 :(得分:4)

我想说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 ::数组大小(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(1核)的轻微加速(串行GD / 715秒)共享GD为665秒。

如果我的实现是正确的(请检查一下 - 我指望这一点)然后GD算法的并行化是不值得的。绝对可以使用单核上的随机GD获得更好的加速度。