Engee 文档
Notebook

CPU和GPU上的神经网络训练比较

让我们回到将多层神经网络训练为回归问题的示例,并比较切换到GPU时该过程将如何加速。

安装库

如果尚未安装命名库,则以下命令可能需要几分钟时间。

In [ ]:
Pkg.add( ["Flux", "CUDA", "cuDNN", "Random"] )
In [ ]:
using Flux, CUDA

准备工作

让我们创建相当大的训练数据向量,如 Float32:

In [ ]:
Nx1, Nx2 = 200, 200
x1 = Float32.( range( -3, 3, length=Nx1 ) )
x2 = Float32.( range( -3, 3, length=Nx2 ) )
Xs = [ repeat( x1, outer=Nx2)  repeat( x2, inner=Nx1) ];
Ys = @. 3*(1-Xs[:,1])^2*exp(-(Xs[:,1]^2) - (Xs[:,2]+1)^2) - 10*(Xs[:,1]/5 - Xs[:,1]^3 - Xs[:,2]^5)*exp(-Xs[:,1]^2-Xs[:,2]^2) - 1/3*exp(-(Xs[:,1]+1) ^ 2 - Xs[:,2]^2);

我们会设定相当长的学习时间。:

In [ ]:
epochs = 2000;
learning_rate = 0.08;

在CPU上运行时,值得入手 epochs=100 并慢慢增加这个量。 即使我们远离"工业"值,当我们在次优设置中训练神经网络时,我们仍然冒着损失大量CPU时间的风险。

CPU上的数据加载和训练

让我们创建一个标准的培训程序:

In [ ]:
model_cpu = Chain( Dense( 2 => 20, relu ), Dense( 20 => 5, relu ), Dense( 5 => 1 ) )
model_copy = deepcopy( model_cpu ) # На будущее нам будет нужна точно такая же модель
data = [ (Xs', Ys') ]
opt_state = Flux.setup( Adam( learning_rate ), model_cpu );
In [ ]:
using Random
Random.seed!( 1 ); # Установим генератор случайных чисел в воспроизводимое состояние
In [ ]:
@time (for i in 1:epochs
    Flux.train!(model_cpu, data, opt_state) do m, x, y
        Flux.mse( m(x), y ) # Лосс функция
    end
end)
467.851443 seconds (20.01 M allocations: 27.787 GiB, 1.23% gc time, 3.11% compilation time)

培训持续了相当长的时间。 如果我们在工作中受到CPU的限制,我们将不得不减少训练样本中的点数,至少在训练开始时,同时我们将选择神经网络的超参数。

这段时间的1-2%用于内存清理(gc_time =垃圾回收)并编译代码(compile time).

将进程传输到GPU

连接库时 Flux,一个建筑出现在我们的处置 |> gpu (将左表达式传递给gpu()函数)。 它允许您在不创建额外的数据处理嵌套级别的情况下直观地将矩阵或结构发送到GPU并在其上执行代码。

In [ ]:
if CUDA.functional()
    model_gpu = model_copy |> gpu;
    
    Xg = Xs' |> gpu;
    Yg = Ys' |> gpu;
    data = [ (Xg, Yg) ]
    
    opt_state = Flux.setup( Adam( learning_rate ), model_gpu );
end;

请注意,我们试图使用 DataLoader. 它的使用显着减慢了GPU上的执行速度,由此得出结论,值得信任Julia GPU编译器实现的并行化过程。

In [ ]:
Random.seed!( 1 ); # Установим генератор случайных чисел в воспроизводимое состояние
In [ ]:
if CUDA.functional()
    @time CUDA.@sync (
        for i in 1:epochs
            Flux.train!(model_gpu, data, opt_state) do m, x, y
                Flux.mse( m(x), y ) # Лосс функция
            end
        end
    )
end
 26.461145 seconds (33.51 M allocations: 1.926 GiB, 2.39% gc time, 80.03% compilation time: 2% of which was recompilation)

GPU迁移总结

通常,将神经网络传输到GPU需要三个额外的操作:

*特征矩阵的转移 Xs |> gpu
*响应矩阵的转移 Ys |> gpu
*使用神经网络传输结构 model |> gpu

通过少量训练数据(100-200个例子),神经网络在CPU上学习得更快。 但对于40,000个示例,优势在于GPU端。

如果减去编译花费的时间(compile time)和执行后的内存清理(gc time =垃圾收集时间)我们可以这样说

*显卡执行此任务的速度比中央处理器快约200倍。

检查结果

我们使用GPU训练的神经网络对原始数据集进行插值。:

In [ ]:
gr()

# Вернем модель на CPU
if CUDA.functional()
    model_gpu = model_gpu |> cpu;
else
    model_gpu = model_cpu
end

# Создадим набор данных поменьше (иначе отрисовка займет очень много времени)
Nx1, Nx2 = 40, 40
x1 = Float32.( range( -3, 3, length=Nx1 ) )
x2 = Float32.( range( -3, 3, length=Nx2 ) )
Xs = [ repeat( x1, outer=Nx2)  repeat( x2, inner=Nx1) ];
Ys = @. 3*(1-Xs[:,1])^2*exp(-(Xs[:,1]^2) - (Xs[:,2]+1)^2) - 10*(Xs[:,1]/5 - Xs[:,1]^3 - Xs[:,2]^5)*exp(-Xs[:,1]^2-Xs[:,2]^2) - 1/3*exp(-(Xs[:,1]+1) ^ 2 - Xs[:,2]^2);

plot(
    surface( Xs[:,1], Xs[:,2], vec(Ys), c=:viridis, cbar=:false, title="Обучающая выборка", titlefont=font(10)),
    wireframe( x1, x2, vec(model_cpu( Xs' )), title="Нейросеть (CPU)", titlefont=font(10) ), 
    wireframe( x1, x2, vec(model_gpu( Xs' )), title="Нейросеть (GPU)", titlefont=font(10) ), 
    layout=(1,3), size=(1000,400)
)
Out[0]:

有趣的是,一个安装 Random.seed 这足以获得两个相同的(外观)结果–一个在CPU上,另一个在GPU上。

结论

我们已经训练了一个神经网络来在CPU和GPU上插值40,000个元素的样本。

GPU上超过2000个周期的学习过程比CPU的学习速度快近200倍,这使您可以在超参数上执行更多迭代并训练更大的网络,从而节省设计人员的时间。

对于100-1000个元素的样本大小,我们没有注意到学习速度的差异,因为由于频繁地从/向GPU传输数据,执行速度的优势消失了。 可能值得组织训练周期,以便数据在整个训练时间内保持在GPU上。