Parallelising gradient calculation in Julia
有人说服我放弃舒适的Matlab编程,然后开始用Julia编程。我从事神经网络工作已经很长时间了,我认为,现在有了Julia,我可以通过并行化梯度计算来更快地完成工作。
无需一口气就整个数据集计算梯度;相反,可以拆分计算。例如,通过将数据集划分为多个部分,我们可以计算每个部分的局部梯度。然后,通过将部分梯度相加来计算总梯度。
虽然原理很简单,但是当我与Julia并行处理时,性能会下降,即一个进程快于两个进程!我显然做错了事...我已经咨询了论坛中提出的其他问题,但是我仍然无法拼凑出答案。我认为我的问题在于,正在传输大量不必要的数据,但是我无法正确地对其进行修复。
为了避免发布混乱的神经网络代码,我在下面发布了一个简单的示例,该示例复制了线性回归设置中的问题。
下面的代码块为线性回归问题创建了一些数据。该代码解释了常量,但是X是包含数据输入的矩阵。我们随机创建一个权重向量w,将其与X相乘会创建一些目标Y。
1 2 3 4 5 6 7 8 9 10 11 12 | ###################################### ## 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的梯度的函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 | #################################### ## 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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 | #################################### ## 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 ",iter, negative_loglikelihood(Y,X,W)) end |
希望可以看到,我在这里尝试以最简单的方式并行化梯度的计算。我的策略是打破梯度计算的范围,尽量减少可用工人的数量。每个工作人员只需要在矩阵X的一部分上工作,该部分由first_index和last_index指定。因此,每个工作人员都应使用
- 工作人员1 => first_index = 1,last_index = 2500
- 工作人员2 => first_index = 2501,last_index = 5000
- 工作人员3 => first_index = 5001,last_index = 7500
- 工作人员4 => first_index = 7501,last_index = 10000
不幸的是,如果我只有一个工人,那么整个代码的工作速度会更快。如果通过
数据项越多,降级就越明显。
在我具有N = 20000和一个内核的特定计算环境中,代码运行时间约为9秒。在N = 20000和4核的情况下,大约需要18秒!
我尝试了许多不同的方法,这些方法都受到了该论坛中的问题和答案的启发,但不幸的是没有结果。我意识到并行化是幼稚的,并且数据移动必定是问题,但是我不知道如何正确地进行。在这个问题上,文档似乎也很少(如Ivo Balbaert的好书)。
非常感谢您的帮助,因为我已经为此苦苦挣扎了很长时间,我在工作中确实需要它。对于想要运行代码的任何人,为您节省复制粘贴的麻烦,您可以在此处获取代码。
感谢您抽出宝贵的时间阅读这个冗长的问题!帮助我将其转变为模型答案,Julia新手可以咨询!
我要说的是,GD不是使用任何建议的方法(
问题不在于朱莉娅,而在于GD算法。
考虑以下代码:
主要过程:
1 2 3 4 | for iter = 1:iterations #iterations:"the more the better" δ = _gradient_descent_shared(X, y, θ) θ = θ - α * (δ/N) end |
问题出在上面的for循环中,这是必须的。不管
阅读问题和以上建议后,我开始使用
主要过程部分(无需正则化的简单实现):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | 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 |
所有工人通用的代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | #= 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)的标签
主要代码可能如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 | 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秒/ 665 sec为共享GD)。
如果我的实现是正确的(请检查一下-我指望这一点),那么GD算法的并行化就不值得了。绝对可以在1核上使用随机GD获得更好的加速。
如果要减少数据移动量,则应强烈考虑使用SharedArrays。您可以只预分配一个输出向量,并将其作为参数传递给每个工作程序。就像您建议的那样,每个工作人员都会设置其中的一部分。