高性能智能计算上机实验报告
并行算法在卷积神经网络的应用
摘要:随着网络数据量的暴增与计算机算力的发展,近些年来深度学习领域取得的重大的发展,许多传统机器学习领域无法解决的问题都在深度学习中取得突破。深度卷积神经网络是深度学习中的一中网络结构,与传统的全连接网络相比,采用卷积实现局部连接和权值共享,能够有效的解决参数爆炸的问题,广泛的被应用在计算机视觉领域。然而由于参数训练过程计算量太大,滑动卷积矩阵乘计算非常的费时,完成一个卷积神经网络模型的训练往往需要消耗大量的时间,针对这个问题,本次实验将构建一个基于CUDA架构的编程环境,采用CUDA/C++编程实现二维的卷积的并行计算,通过对比GPU实现与CPU实现,调整不同参数,分析并行技术对程序性能的提升效果。
1 实验背景
深度学习(Deep Learning)从2006年Hinton在Science上的论文算起,深度学习发展至今才十年。在这短短几年时间里,深度学习颠覆了语音识别、图像分类、文本理解等众多领域的算法设计思路,渐渐形成了一种从训练数据出发,经过一个端到端(end-to-end)的模型,然后直接输出得到最终结果的一种新模式。这不仅让一切变得更加简单,而且由于深度学习中的每一层都可以为了最终的任务来调整自己,最终实现各层之间的通力 合作,因而可以大大提高任务的准确度。随着大数据时代的到来以及GPU等各种更加强大的计算设备的发展,深度学习如虎添翼,可以充分利用各种海量数据(标注数据、弱标注。数据或者仅仅数据本身),完全自动地学习到抽象的知识表达,即把原始数据浓缩成某种知识。
卷积神经网络是人工神经网络的一种,当前已经成为了图像识别领域的研究热点。卷积神经网络最突出的两个特点是其神经元感受野结构和神经元间的权值共享。正是由于这两个特殊的设计,使得其相对一般的人工神经网络,有更好的对图像平移、缩放、旋转等不变性。与普通卷积神经网络不同,卷积神经网络的神经元特有感受野的结构,网络中的每个神经元都只与其前一层的局部相连,也就是每个神经元只处理图像的局部信息。权值共享则是卷积神经网络的另外一个重要特性,权值共享特性是让一个特征平面内的所有神经元都共用相同的权值和偏置系数。显然共享权值使得每个特征平面只能提取图像中的一种特征,这种不足可以很好地通过每层网络使用多个特征平面来很好的弥补。相比全连接的神经网络,拥有权值共享特性的卷积神经网络能够大幅减少需要训练的参数。这不但减少了网络训练的时间,同时使得卷积神经网络的训练变得更容易。
近几年GPU通用并行技术得到了飞速的发展,因为其计算核心的数量远远多于CPU,在高度并行的计算问题上,GPU的计算效率要远高于CPU。CUDA是由英伟达公司推出的GPU通用并行计算平台,不仅提供了一系列API使得开发人员能够绕过GPU的图形API来直接访问GPU的硬件,还提供了一个C语言编译器,使得开发人员能够使用C语言来进行快速的开发。基于CUDA的代码就可以运行在GPU上,使得卷积神经网络算法在GPU上的开发成为可能。
2 实验原理
卷积神经网络中有很多计算都是矩阵与向量之间、矩阵之间的运算,大量的矩阵运算是造成模型训练时间过长的主要原因,而矩阵运算具有并行特征,CUDA非常适合做矩阵运算,因此如果在GPU上执行计算过程,可以达到提高计算速度的目的。
卷积神经网络的经典AlexNet结构如下图:
图中很好的反应出卷积神经网络的向前传导过程,以227*227*3大小的RGB图像为例,经过padding(填白)、stride(设置步长)、kernels(卷积核)、pooling(池化)等一系列过程,在多个kernels的作用下慢慢的结构变得扁平化,最后以全连接网络的形式结尾。采取这种结构的好处主要有两处,1)权值共享:即整个像素矩阵共享同一个卷积中的参数,这样有效的减少了参数的数量,而且可以检测出多个地方的相同特征。2)局部连接:即输出只与输入的局部一部分有关,符合图像局部特征。正是由于这两个优点使得CNN超越了传统的NN,开启了神经网络的新时代。
卷积核也可以叫做滤波器,不同领域叫法不同,经过多次迭代可以达到提取特征的作用,本质上是找到图片上与卷积核本身相似的部分。卷积运算本质上是卷积核与输入数据的同等矩阵进行矩阵运算。取深度卷积神经网络中一层为例,可绘制如下图结构:
输入数据在多个卷积核的作用下,经过矩阵运算和激活,长度宽度减小,却慢慢变长。在深层卷积的作用下,模型最终可以抽取出复杂的特征,可以完成复杂的任务。
3 实验运行环境
硬件:
CPU:lntel(R) Core(TM) i7-10700 CPu @ 2.90GHz2.90 GHz
GPU:GeForce GTX 1660 SUPER
软件:Visual Studio 2019
操作系统:Windows 10 家庭中文版
编译器:cl.exe
开发语言及相关软件:c++,cuda-10.2.89
辅助软件:
4 实验内容
本次实验选取二维卷积操作进行并行化设计。如下图为一个标准的二维卷积运算,非并行算法卷积运算都是采取滑动窗口对图像进行处理,而采用GPU可以实现并行。
首先利用CUDA架构的接口获取本机设备GPU的参数如下,可以看到本机搭载了一块GPU,块内共享内存大小为49152,块内寄存器大小为65536,现场束32,块内最大线程数1024,已及两个dim3数据。
了解了硬件基础开始算法设计,本次实验采取对比形式,在系统内存中申请卷积操作的输入数组,输出数组及卷积核数组,而后通过CUDA接口向GPU显存传输数据,而后,在GPU端开启多个线程进行并行计算。另外,在CPU端实现同样功能的串行代码作为GPU并行计算的性能参考版本。
CPU代码设计如下:
void Conv2(float** filter, float** arr, float** res, int filter_size, int arr_size) {
int temp;
for (int i = 0; i < arr_size; i++) {
for (int j = 0; j < arr_size; j++) {
temp = 0;
int starti = i - filter_size / 2;
int startj = j - filter_size / 2;
for (int m = starti; m < starti + filter_size; m++) {
for (int n = startj; n < startj + filter_size; n++) {
if (m >= 0 && m < arr_size && n >= 0 && n < arr_size) {
temp += filter[m - starti][n - startj] * arr[m][n];
}
}
}
res[i][j] = temp;
}
}
}
在设计GPU代码前,首先必须要了解CUDA在软件层面的线程组织结构,这样才能更好的设计并行算法,其组织结构图如下:
这个是Grid与Block都是二维的情况,当然也可以是一维或三维结构,但二维更为直观,使用也更方便,于是本次实验采用的是dim为2的情况。虽然有嵌套的关系,线程号计算公式也并不复杂,如果将所有的Block都展开,其实就是一个更大的二维矩阵。
于是本次实验卷积计算的并行设计为,每次与卷积核的线性和由一个线程完成,设置好二维Block的大小,并根据图像矩阵计算出要用的块数矩阵,然后将分好的线程组织全部展开,安装展开后线程矩阵与图片矩阵的对应关系分配线程。
GPU代码如下:
核函数:
__global__
void convolution_2D_basic(float* in, float* out, float* mask, int maskwidth, int w, int h) {
int Col = blockIdx.x * blockDim.x + threadIdx.x;
int Row = blockIdx.y * blockDim.y + threadIdx.y;
if (Row < h && Col < w) {
float pixVal = 0;
//start
int startCol = Col - maskwidth / 2;
int startRow = Row - maskwidth / 2;
//caculate the res
for (int i = 0; i < maskwidth; i++)
{
for (int j = 0; j < maskwidth; j++)
{
int curRow = startRow + i;
int curCol = startCol + j;
if (curRow > -1 && curRow<h && curCol>-1 && curCol < w)
{
pixVal += mask[i * maskwidth + j] * in[curRow * w + curCol];
}
}
}
out[Row * w + Col] = pixVal;
}
}
线程分配及调用:
dim3 block(threadPerBlockX, threadPerBlockY);
dim3 grid((arr_size-1) / threadPerBlockX + 1,(arr_size-1) / threadPerBlockY + 1);
convolution_2D_basic << <grid, block >>>(inD, outD, maskD, filter_size, arr_size, arr_size);
实验结果如下
实验 | 一 | 二 | 三 | 四 | 五 | 六 |
像素矩阵 | 512*512 | 1024*1024 | 2048*2048 | 4096*4096 | 8192*8192 | 16384*16384 |
卷积核 | 5*5 | 5*5 | 5*5 | 5*5 | 5*5 | 5*5 |
线程数/block | 16*16 | 16*16 | 16*16 | 16*16 | 16*16 | 16*16 |
block 数 | 32*32 | 64*64 | 128*128 | 256*256 | 512*512 | 1024*1024 |
总线程数 | 512*512 | 1024*1024 | 2048*2048 | 4096*4096 | 8192*8192 | 16384*16384 |
GPU时间 | 39 | 152 | 644 | 2472 | 9687 | 38910 |
CPU时间 | 0.003795 | 0.004003 | 0.004550 | 0.004985 | 0.008501 | 0.025341 |
加速比 | 10277 | 37969 | 141537 | 495853 | 1139546 | 1535440 |
从结果可以看出,当橡素矩阵成倍增加时,CPU串行串行的运行时间也在成倍增加,而gpu并行程序的时间在前四组的时间稍微有一点增长,但变化不大,而在后面两组实验中时间变化很大。原因可能时当线程数少时,gpu资源可以满足所有的线程同时并行,此时时间增加不大,而当线程数过多,寄存器、共享内存等稀有资源不足时,此时很多线程要等待资源空闲才能执行,即硬件资源以及每个线程所需资源决定了gpu的最大线程并行数。
5 实验总结
本此实验花费了很长的时间,首先是把高性能计算的PPT全部看了一遍,回顾了先前学过的pthread、openmp、mpi,mpi是基于分布式内存系统,而openmp和pthread基于共享内存系统。mpi之间的数据共享需要通过消息传递,因为mpi同步的程序属于不同的进程, 相反由于openmp和pthread共享内存,不同线程之间的数据就无须传递,直接传送指针就行。然后进入了cuda的学习,在csdn上找了一篇很详细的博客,看了好几遍才大概对cuda有一个细致的了解。最后选择做卷积运算的并行设计是借助了先前实现矩阵乘法并行设计的思路,并且目前卷积神经网络在深度学习中运用非常广泛,且存在着大量矩阵运算导致训练时间过长的问题,借助gpu的强大算力可以实现更加深层次网络的训练。
实验中也遇到了一些问题。首先就是在cuda中没有二维数组的概念,因此需要将矩阵实际存储为一维,然后进行坐标的换算。其次就是gpu上计时的问题,必须采用高精度的计时方法才能准确的计算出时间,可以采用gpu事件计时或者QueryPerformanceCounter等高精度计时器。还有就是在线程分配时的问题,一方面是在卷积时矩阵维度会变小,一些线程可能会空闲,还有在分配块时也会出现线程空闲,导致占有率下降,可以在块计算时设计合理一些,当然这个问题并不会影响实验的分析结果。
从这么课中主要学习到了并行的思想,为以后编写程序供了一种思考的方向。