我是程序员Python科研成长时

仅使用NumPy完成卷积神经网络CNN的搭建(附Python代码

2018-05-02  本文已影响465人  阿里云云栖号

摘要: 现有的Caffe、TensorFlow等工具箱已经很好地实现CNN模型,但这些工具箱需要的硬件资源比较多,不利于初学者实践和理解。因此,本文教大家如何仅使用NumPy来构建卷积神经网络(Convolutional Neural Network , CNN)模型,具体实现了卷积层、ReLU激活函数层以及最大池化层(max pooling),代码简单,讲解详细。

目前网络上存在很多编译好的机器学习、深度学习工具箱,在某些情况下,直接调用已经搭好的模型可能是非常方便且有效的,比如Caffe、TensorFlow工具箱,但这些工具箱需要的硬件资源比较多,不利于初学者实践和理解。因此,为了更好的理解并掌握相关知识,最好是能够自己编程实践下。本文将展示如何使用NumPy来构建卷积神经网络(Convolutional Neural Network , CNN)。

CNN是较早提出的一种神经网络,直到近年来才变得火热,可以说是计算机视觉领域中应用最多的网络。一些工具箱中已经很好地实现CNN模型,相关的库函数已经完全编译好,开发人员只需调用现有的模块即可完成模型的搭建,避免了实现的复杂性。但实际上,这样会使得开发人员不知道其中具体的实现细节。有些时候,数据科学家必须通过一些细节来提升模型的性能,但这些细节是工具箱不具备的。在这种情况下,唯一的解决方案就是自己编程实现一个类似的模型,这样你对实现的模型会有最高级别的控制权,同时也能更好地理解模型每步的处理过程。

本文将仅使用NumPy实现CNN网络,创建三个层模块,分别为卷积层(Conv)、ReLu激活函数和最大池化(max pooling)。

1.读取输入图像

       以下代码将从skimage Python库中读取已经存在的图像,并将其转换为灰度图:

1. import skimage.data

2. # Reading the image

3. img = skimage.data.chelsea()

4. # Converting the image into gray.

5. img = skimage.color.rgb2gray(img)js

 读取图像是第一步,下一步的操作取决于输入图像的大小。将图像转换为灰度图如下所示:

2.准备滤波器

       以下代码为第一个卷积层Conv准备滤波器组(Layer 1,缩写为l1,下同):

1.  l1_filter = numpy.zeros((2,3,3))

根据滤波器的数目和每个滤波器的大小来创建零数组。上述代码创建了2个3x3大小的滤波器,(2,3,3)中的元素数字分别表示2:滤波器的数目(num_filters)、3:表示滤波器的列数、3:表示滤波器的行数。由于输入图像是灰度图,读取后变成2维图像矩阵,因此滤波器的尺寸选择为2维阵列,舍去了深度。如果图像是彩色图(具有3个通道,分别为RGB),则滤波器的大小必须为(3,3,3),最后一个3表示深度,上述代码也要更改,变成(2,3,3,3)。

滤波器组的大小由自己指定,但没有给定滤波器中具体的数值,一般采用随机初始化。下列一组值可以用来检查垂直和水平边缘:

3.卷积层(Conv Layer)

       构建好滤波器后,接下来就是与输入图像进行卷积操作。下面代码使用conv函数将输入图像与滤波器组进行卷积:

1. l1_feature_map = conv(img, l1_filter)

conv函数只接受两个参数,分别为输入图像、滤波器组:

   该函数首先确保每个滤波器的深度等于图像通道的数目,代码如下。if语句首先检查图像与滤波器是否有一个深度通道,若存在,则检查其通道数是否相等,如果匹配不成功,则报错。

1. if len(img.shape) > 2 or len(conv_filter.shape) > 3: # Check if number of image channels matches the filter depth.

2. if img.shape[-1] != conv_filter.shape[-1]:

3. print("Error: Number of channels in both image and filter must match.")

此外,滤波器的大小应该是奇数,且每个滤波器的大小是相等的。这是根据下面两个if条件语块来检查的。如果条件不满足,则程序报错并退出。

1. if conv_filter.shape[1] != conv_filter.shape[2]: # Check if filter dimensions are equal.

2. print('Error: Filter must be a square matrix. I.e. number of rows and columns must match.')

3. sys.exit()

4. if conv_filter.shape[1]%2==0: # Check if filter diemnsions are odd.

5. print('Error: Filter must have an odd size. I.e. number of rows and columns must be odd.')

6. sys.exit()

上述条件都满足后,通过初始化一个数组来作为滤波器的值,通过下面代码来指定滤波器的值:

1. # An empty feature map to hold the output of convolving the filter(s) with the image. 2. feature_maps = numpy.zeros((img.shape[0]-conv_filter.shape[1]+1,

3. img.shape[1]-conv_filter.shape[1]+1,

4. conv_filter.shape[0]))

由于没有设置步幅(stride)或填充(padding),默认为步幅设置为1,无填充。那么卷积操作后得到的特征图大小为(img_rows-filter_rows+1, image_columns-filter_columns+1, num_filters),即输入图像的尺寸减去滤波器的尺寸后再加1。注意到,每个滤波器都会输出一个特征图。

循环遍历滤波器组中的每个滤波器后,通过下面代码更新滤波器的状态:

1. curr_filter = conv_filter[filter_num, :] # getting a filter from the bank.

如果输入图像不止一个通道,则滤波器必须具有同样的通道数目。只有这样,卷积过程才能正常进行。最后将每个滤波器的输出求和作为输出特征图。下面的代码检测输入图像的通道数,如果图像只有一个通道,那么一次卷积即可完成整个过程:

1. if len(curr_filter.shape) > 2:

2. conv_map = conv_(img[:, :, 0], curr_filter[:, :, 0]) # Array holding the sum of all feature map

3. for ch_num in range(1, curr_filter.shape[-1]): # Convolving each channel with the image and summing the results.

4. conv_map = conv_map + conv_(img[:, :, ch_num],

5. curr_filter[:, :, ch_num])

6. else: # There is just a single channel in the filter.

7. conv_map = conv_(img, curr_filter)

上述代码中conv_函数与之前的conv函数不同,函数conv只接受输入图像和滤波器组这两个参数,本身并不进行卷积操作,它只是设置用于conv_函数执行卷积操作的每一组输入滤波器。下面是conv_函数的实现代码:

每个滤波器在图像上迭代卷积的尺寸相同,通过以下代码实现:

1. curr_region = img[r:r+filter_size, c:c+filter_size]

之后,在图像区域矩阵和滤波器之间对位相乘,并将结果求和以得到单值输出:

1. #Element-wise multipliplication between the current region and the filter.

2. curr_result = curr_region * conv_filter

3. conv_sum = numpy.sum(curr_result) #Summing the result of multiplication.

4. result[r, c] = conv_sum #Saving the summation in the convolution layer feature map.

 输入图像与每个滤波器卷积后,通过conv函数返回特征图。下图显示conv层返回的特征图(由于l1卷积层的滤波器参数为(2,3,3),即2个3x3大小的卷积核,最终输出2个特征图):

卷积后图像

卷积层的后面一般跟着激活函数层,本文采用ReLU激活函数。

4.ReLU激活函数层

       ReLU层将ReLU激活函数应用于conv层输出的每个特征图上,根据以下代码行调用ReLU激活函数:

l1_feature_map_relu= relu(l1_feature_map)

ReLU激活函数(ReLU)的具体实现代码如下:

1.defrelu(feature_map):

2.#Preparing the output of the ReLU activation function. 

3.relu_out = numpy.zeros(feature_map.shape)

4.formap_numinrange(feature_map.shape[-1]):

5.forrinnumpy.arange(0,feature_map.shape[0]):6.forcinnumpy.arange(0, feature_map.shape[1]):7.relu_out[r, c, map_num] = numpy.max(feature_map[r, c, map_num],0)

ReLU思想很简单,只是将特征图中的每个元素与0进行比较,若大于0,则保留原始值。否则将其设置为0。ReLU层的输出如下图所示:

ReLU层输出图像

激活函数层后面一般紧跟池化层,本文采用最大池化(max pooling)。

5.最大池化层

       ReLU层的输出作为最大池化层的输入,根据下面的代码行调用最大池化操作:

1.  l1_feature_map_relu_pool = pooling(l1_feature_map_relu, 2, 2)

最大池化函数(max pooling)的具体实现代码如下:

1.defpooling(feature_map, size=2, stride=2):

2.#Preparing the output of the pooling operation. 

3.pool_out = numpy.zeros((numpy.uint16((feature_map.shape[0]-size+1)/stride),

4.numpy.uint16((feature_map.shape[1]-size+1)/stride),

5.feature_map.shape[-1]))

6.formap_numinrange(feature_map.shape[-1]):

7.r2 =0

8.forrinnumpy.arange(0,feature_map.shape[0]-size-1, stride):

9.c2 =0

10.forcinnumpy.arange(0, feature_map.shape[1]-size-1, stride):

11.pool_out[r2, c2, map_num] = numpy.max(feature_map[r:r+size,  c:c+size])

12.c2 = c2 +113.r2 = r2 +1

       该函数接受3个参数,分别为ReLU层的输出,池化掩膜的大小和步幅。首先也是创建一个空数组,用来保存该函数的输出。数组大小根据输入特征图的尺寸、掩膜大小以及步幅来确定。

1.  pool_out = numpy.zeros((numpy.uint16((feature_map.shape[0]-size+1)/stride),2.                          numpy.uint16((feature_map.shape[1]-size+1)/stride),3.                          feature_map.shape[-1]))

       对每个输入特征图通道都进行最大池化操作,返回该区域中最大的值,代码如下:

pool_out[r2, c2, map_num] = numpy.max(feature_map[r:r+size,  c:c+size])

池化层的输出如下图所示,这里为了显示让其图像大小看起来一样,其实池化操作后图像尺寸远远小于其输入图像。

池化层输出图像

6.层堆叠

       以上内容已经实现CNN结构的基本层——conv、ReLU以及max pooling,现在将其进行堆叠使用,代码如下:

1.  # Second conv layer

2.  l2_filter = numpy.random.rand(3, 5, 5, l1_feature_map_relu_pool.shape[-1])

3.  print("\n**Working with conv layer 2**")

4.  l2_feature_map = conv(l1_feature_map_relu_pool, l2_filter)

5.  print("\n**ReLU**")

6.  l2_feature_map_relu = relu(l2_feature_map)

7.  print("\n**Pooling**")

8.  l2_feature_map_relu_pool = pooling(l2_feature_map_relu, 2, 2)

9.  print("**End of conv layer 2**\n")

从代码中可以看到,l2表示第二个卷积层,该卷积层使用的卷积核为(3,5,5),即3个5x5大小的卷积核(滤波器)与第一层的输出进行卷积操作,得到3个特征图。后续接着进行ReLU激活函数以及最大池化操作。将每个操作的结果可视化,如下图所示:

l2层处理过程可视化图像  

1.  # Third conv layer

2.  l3_filter = numpy.random.rand(1, 7, 7, l2_feature_map_relu_pool.shape[-1])

3.  print("\n**Working with conv layer 3**")

4.  l3_feature_map = conv(l2_feature_map_relu_pool, l3_filter)

5.  print("\n**ReLU**")

6.  l3_feature_map_relu = relu(l3_feature_map)

7.  print("\n**Pooling**")

8.  l3_feature_map_relu_pool = pooling(l3_feature_map_relu, 2, 2)

9.  print("**End of conv layer 3**\n"

从代码中可以看到,l3表示第三个卷积层,该卷积层使用的卷积核为(1,7,7),即1个7x7大小的卷积核(滤波器)与第二层的输出进行卷积操作,得到1个特征图。后续接着进行ReLU激活函数以及最大池化操作。将每个操作的结果可视化,如下图所示:

       神经网络的基本结构是前一层的输出作为下一层的输入,比如l2层接收l1层的输出,l3层接收来l2层的输出,代码如下:

1.  l2_feature_map = conv(l1_feature_map_relu_pool, l2_filter)

2.  l3_feature_map = conv(l2_feature_map_relu_pool, l3_filter)

7.完整代码

作者信息

Ahmed Gad,研究兴趣是深度学习、人工智能和计算机视觉

本文由阿里云云栖社区组织翻译。

文章原标题《Building Convolutional Neural Network using NumPy from Scratch》,译者:海棠,审校:Uncle_LLD。

详情请阅读原文

上一篇下一篇

猜你喜欢

热点阅读