手把手带你用Numpy实现CNN <零>

Numpy是一个非常好用的python科学计算的库,CNN是现在视觉领域深度学习的基础之一。虽然好的框架很多,不过自己用Numpy实现一个可以使用的CNN的模型有利于初学者加深对CNN的理解。

本文旨在通过这样一个专栏(文章) 介绍如何用Numpy从零实现一个可以训练的CNN简易网络,同时对深度学习(CNN)的相关基础知识进行一些复习,也希望能够给正在入门的学弟学妹们一些简单的归纳。

看干货请跳过<内容介绍>和<开始之前>或者直接踹门去我的github wuziheng

内容介绍:

进入正题:通过完成这篇(系列)文章,希望自己能够讲清楚以下几件事:

  • Numpy实现基本的CNN网络组件:conv, pooling, relu, fullyconnect, softmax
LeNet网络结构图
基本组件列表
  • 从实现的基本组件搭建可以训练的网络,完成mnist的训练与测试,画出一个下图的训练曲线

实现的mnist的训练曲线

  • 从贯序连接的层模型到计算图模型的引入,完成自动求值,求导等功能
  • 常见的初始化方法,激活函数,优化方法,Loss函数的实现与比较,例如relu系与sigmoid系的比较,sgd,momentum,Adam的比较等等
  • 常见的trick的实现,例如dropout, batchnorm, residual

实际上,这些东西tensorflow的源代码都有.一开始我只是看到某公司笔试题然后花了一到两天完成了第一个的内容。随后愈发觉得自己都写一下也蛮好玩的,有空的同学可以一起玩一玩,当然觉得这些都烂熟于心的大佬们,你们可以跳过~

文章之前的工作的timeline和详细的代码都可以在我的github:https://github.com/wuziheng/CNN-Numpy上看到,已经实现的有1~4的大部分&dropout。

开始之前:

正式开始之前,先讲一讲为什么会写这个小项目。我相信在深度学习这么火爆的前提下,一定有很多同学和我一样是赶鸭子上架自学踹门而入的。比如我从16年12月开始接触深度学习,17年3月春节回校之后在小老板的要求下开始在Nvidia的X1+k80上进行一些网络和框架的试验(因为实验室不做学术+外加老板之前不是做这个的)所以之后自己抽空参加了一些比赛,继续踹门学习至今。

从赶鸭子上架到踹门自学

一整年下来,搬运了很多脚本,更多的时候比较迷茫,在入门的很长一段时间内对于深度学习还总是有一种雾里看花的感觉,训练~测试~敲里吗~训练~测试~敲里吗~循环

后来我意识到,迷茫可能是因为自己浅尝辄止,遇事多以完成目标为止。只有自己去写了,了解自己的每一行代码,才能慢慢的消除这种感觉。所以才萌生了自己实现这些基本的算法的想法,正好新一届学弟们也开始入门,我也想留下一点自己小小的经验~希望能够帮助到其他人。

需求

  1. 如果你会一点Python(~不会也没关系,贼简单)
  2. 如果你也想入门深度学习(~大概知道lenet+bp就可以了)

那我想你一定可以和我一起实现这个每一行代码都属于自己的简易CNN框架。

Part I 卷积神经网络的基本组件

绝大部分的同学入门深度学习,第一个接触的应该就是LeNet,我们也将以此为例子介绍卷积神经网络的基本组件。参考上面的的网络结构图,它包含了卷积层(Convolutions),池化层(pooling), 全连接层(Full connection)。

卷积层(Convolutions)

卷积:如果你有图像处理的基础,对于卷积操作我想你一定不会陌生。在传统的图像处理中,卷积操作多用来进行滤波,锐化或者边缘检测啥的。我们可以认为卷积是利用某些设计好的参数组合(卷积核)去提取图像空域上相邻的信息。

\[S(i,j)=(I*K)(i,j)=\sum_{m}\sum_{n}I(m,n)K(i-m,j-n)\]

卷积计算图解

在二维图像上,卷积操作一方面可以高效地按照我们的需求提取图像的邻域信息,在全局上又有着非常好的平移等变性,简单就是说你将输入图像的某一部分移动到另外一部分,在输出图像上也会有着相应的移动。比如下图是花书里用来阐述卷积在特征提取的效率优势上的一张图片,我们用来演示卷积的效果。假设我们把狗狗的鼻子移动到左上角,那么相应输出里面的狗鼻子也会出现在左上角。(照片来源Paula Goodfellow)

卷积计算效果图

卷积神经网络之所以非常适合处理视觉问题,卷积的平移等变性是一大功臣。卷积神经网络中的卷积层可以理解成是在二维图像卷积的基础上增广而来。在二维图像卷积操作里,对于一个固定的卷积核,它能够提取输入图像的某种特征,但在实际的视觉问题里,某一个尺度下的某种空间特征不足以解决我们的需求。所以我们需要改进原始的卷积操作使得可以提取不同尺度下的不同特征。

  1. 一次操作(一层)中使用多个卷积核得到该尺度下的多张特征映射
  2. 多层(次)提取不同尺度下的不同特征信息

由于第一点改进,自然而然,即使第一张图片输入只有一个通道,后面其他层的输入都是多通道。所以对应的我们的卷积核也是多通道。即输入图像和卷积核都添加了channel这个维度,那么卷积层中的卷积操作变为了如下的定义:

\[S(i,j,c)=(I*K_c)(i,j)=\sum_{c}\sum_{m}\sum_{n}I(m,n,c)K_c(i-m,j-n,c)\]

到这,我们大概可以总结出一个卷积层的前向计算(Forward)和他的功能。当然正如我们前面提到过,需要读者懂一点BP算法,因为即使我们知道如何通过卷积层提取输入图像的特征,卷积层依旧无法正常的工作,因为卷积操作最关键的部分卷积核的数值没有被确定下来。BP算法就是告诉我们,如何通过监督学习的方法来优化我们的卷积核的数值,使得我们能够找到在对应任务下表现最好的卷积核(特征),当然这个说法不是很准确。所以在我们实现的卷积层的类中,还会包含一个Backward方法,用于反向传播求导。考虑到一次篇幅不要太长这一部分的原理和实现将放在下一篇文章里。

在这篇接下来的部分里我们就将逐一用Numpy实现一个可以运行的Conv2D类以及Forward方法。

Show me your code

PS:文章看到的版本是不基于graph模型的,Conv2D直接继承自Object,所有的数据和操作都是裸露的,单纯为了实现功能。github上面这一部分代码已经不用了,放在layers文件夹下。

初始化

根据上面的公式,我们知道实现一个卷积前向计算的操作,我们需要知道以下信息:

  1. 输入数据的shape = [N,W,H,C] N=Batchsize/W=width/H=height/C=channels
  2. 卷积核的尺寸ksize ,个数output_channels, kernel shape [output_channels,k,k,C]
  3. 卷积的步长,基本默认为1.
  4. 卷积的方法,VALID or SAME,即是否通过padding保持输出图像与输入图像的大小不变

实际上还需要知道核参数的初始化方法

class Conv2D(object):    def __init__(self, shape, output_channels, ksize=3, stride=1, method='VALID'):        self.input_shape = shape        self.output_channels = output_channels        self.input_channels = shape[-1]        self.batchsize = shape[0]        self.stride = stride        self.ksize = ksize        self.method = method        weights_scale = math.sqrt(ksize*ksize*self.input_channels/2)        self.weights = np.random.standard_normal(    (ksize, ksize, self.input_channels, self.output_channels)) / weights_scale        self.bias = np.random.standard_normal(self.output_channels) / weights_scale

我们通过初始化函数确定上面提到的所需参数,在第一次声明的时候完成了对该层的构建。如下:

conv1 = Conv2D([batch_size, 28, 28, 1], 12, 5, 1)

这个conv1的实例就代表了该层,自然也会包含该层所需要的参数,例如kernel weights, kernel bias,我们用 \[np.random.standard\_normal(kernel\_shape)\] 生成对应的kernel weights和kernel bias。因为 \[np.random.standard\_normal()\]  生成的mean=0,stdev=1的随机Numpy数组,这里我们后面除以相应的weights_scale(msra方法)去控制一下初始化生成的weights的stdev,好的初始化可以加速收敛。

下面这一部分是反向传播中用到的,这篇文章中暂时不会用到,self.eta用于储存backward传回来的 \[\frac{\partial loss}{\partial conv\_out}\]  他与该层的out是一个同样维度的数组.

这里我们就可以看到method时如何控制输出数据的形状的。“SAME”就表示添加padding使得输出长宽不变。self.w_gradient,self.b_gradient则分别用于储存backward计算过后得到的该次的 \[\frac{\partial loss}{\partial weights},\frac{\partial loss}{\partial bias}\]

if method == 'VALID':    self.eta = np.zeros((shape[0], (shape[1] - ksize ) / self.stride + 1, (shape[1] - ksize ) / self.stride + 1,     self.output_channels)) 
if method == 'SAME':    self.eta = np.zeros((shape[0], shape[1]/self.stride, shape[2]/self.stride,self.output_channels)) 
self.w_gradient = np.zeros(self.weights.shape) self.b_gradient = np.zeros(self.bias.shape) self.output_shape = self.eta.shape

前向计算(forward)

如何实现卷积层前向计算,是一个非常老生长谈的问提,譬如我唯一一次面试(尴尬的经历):

面试官:     ”...天南...海北...“ 面试官:     ”你知道卷积是怎么算的嘛?“(原话) 懵逼的我:   ”还能怎么算~?~不就是点乘么?“ 面试官:     ~懵逼~ 我:         ~懵逼~

回头面试挂了,我才意识到他可能是想问我讲一下im2col,然后矩阵再乘法。或者用cuda自己实现一个并行的卷积计算(比如说X1的线程数就很适合一次处理一张1080p图像的一行)。实际上在这次非常尴尬的面试之前,上面说的这两种方法我都自己实现过,可我就是不太明白面试官要问啥~这里我要借这个机会吐槽一下这个面试官,下次可不可有点铺垫,再明确问卷积层的计算有哪些优化的方法?~

回到正题:CNN中的卷积层是如何计算?

贾扬清大神比我回答的好多了,参考这个链接 在Caffe中如何计算卷积

这里主要使用的就是im2col优化方法:通过将图像展开,使得卷积运算可以变成两个矩阵乘法

图解im2col

参见论文 High Performance Convolutional Neural Networks for Document Processing

我们的forward方法的实现基本就是实现了上图,主要分为以下四个步骤:

完整的代码如下(因为我不会把list和代码块间隔放只能分开了)

def forward(self, x):    col_weights = self.weights.reshape([-1, self.output_channels])    if self.method == 'SAME':        x = np.pad(x, ((0, 0), (self.ksize / 2, self.ksize / 2), (self.ksize / 2, self.ksize / 2), (0, 0)),                         'constant', constant_values=0)    self.col_image = []    conv_out = np.zeros(self.eta.shape)    for i in range(self.batchsize):        img_i = x[i][np.newaxis, :]        self.col_image_i = im2col(img_i, self.ksize, self.stride)        conv_out[i] = np.reshape(np.dot(self.col_image_i, col_weights) + self.bias, self.eta[0].shape)        self.col_image.append(self.col_image_i)    self.col_image = np.array(self.col_image)    return conv_out
  1. 首先我们将卷积层的参数weights通过ndarray自带的reshape方法reshape到上图中Kernal Matrix的形状。
  2. 根据self.method,选择是否对输入的数据进行padding,这里我们调用 \[np.pad()\]  方法,对我们的输入数据四维ndarray的第二维和第三维分别padding上与卷积核大小相匹配的0元素
  3. 声明一个list用于存储转换为column的image,在backward中我们还会用到
  4. 对于batch中的每一个数据,分别调用im2col方法,将该数据转化为上图中的Input features(Matrix), 然后调用 \[np.dot()\]  完成矩阵乘法得到Output features(Matrix), reshape输出的shape,填充到输出数据中。

im2col的代码如下:

def im2col(image, ksize, stride):    # image is a 4d tensor([batchsize, width ,height, channel])    image_col = []    for i in range(0, image.shape[1] - ksize + 1, stride):        for j in range(0, image.shape[2] - ksize + 1, stride):            col = image[:, i:i + ksize, j:j + ksize, :].reshape([-1])            image_col.append(col)    image_col = np.array(image_col)    return image_col    

到这里我们就基本实现了简单的卷积层的forward方法,你可以通过输入图片,指定weights的值,进行简单的运算,测试是否能够正确的进行前向的计算,完整的代码可以去我的github直接看最新版,虽然有很较大的出入,但一样是非常容易理解的。我们也能看出,之所以选用python+Numpy实现,是因为大多数需要用到的方法,例如reshape,pad,dot,都已经在Numpy中实现好了,同时,也很方便我们实时的去检查。

小结:考虑到篇幅问题,第一篇文章里就只介绍到这里。整篇文章基本上是以图像的视角,介绍了如何利用Numpy实现CNN中的卷积层的前向计算。这只是CNN精彩的地方,但不是最关键的地方,下一篇中我们将先介绍BP算法以及从机器学习相关视角来看待CNN,然后实现卷积层里面的backward()与apply_gradient()。当然也欢迎直接去我的githubCNN-Numpy可以看到迄今为止实现的所有内容,欢迎大家的宝贵意见~

手把手带你Numpy实现CNN
手把手带你Numpy实现CNN

Numpy是一个非常好用的python科学计算的库,CNN是现在视觉领域深度学习的基础之一。虽然到2019年,好的框架已经有很多,不过自己用Numpy实现一个可以使用的CNN的模型有利于初学者加深对CNN的理解。本文旨在通过这样一个专栏(文章) 介绍如何用Numpy从零实现一个可以训练的CNN简易网络,同时对深度学习(CNN)的相关基础知识进行一些复习。

入门卷积神经网络CNN深度学习Numpy
3
相关数据
深度学习技术

深度学习(deep learning)是机器学习的分支,是一种试图使用包含复杂结构或由多重非线性变换构成的多个处理层对数据进行高层抽象的算法。 深度学习是机器学习中一种基于对数据进行表征学习的算法,至今已有数种深度学习框架,如卷积神经网络和深度置信网络和递归神经网络等已被应用在计算机视觉、语音识别、自然语言处理、音频识别与生物信息学等领域并获取了极好的效果。

池化技术

池化(Pooling)是卷积神经网络中的一个重要的概念,它实际上是一种形式的降采样。有多种不同形式的非线性池化函数,而其中“最大池化(Max pooling)”是最为常见的。它是将输入的图像划分为若干个矩形区域,对每个子区域输出最大值。直觉上,这种机制能够有效的原因在于,在发现一个特征之后,它的精确位置远不及它和其他特征的相对位置的关系重要。池化层会不断地减小数据的空间大小,因此参数的数量和计算量也会下降,这在一定程度上也控制了过拟合。通常来说,CNN的卷积层之间都会周期性地插入池化层。

激活函数技术

在 计算网络中, 一个节点的激活函数定义了该节点在给定的输入或输入的集合下的输出。标准的计算机芯片电路可以看作是根据输入得到"开"(1)或"关"(0)输出的数字网络激活函数。这与神经网络中的线性感知机的行为类似。 一种函数(例如 ReLU 或 S 型函数),用于对上一层的所有输入求加权和,然后生成一个输出值(通常为非线性值),并将其传递给下一层。

机器学习技术

机器学习是人工智能的一个分支,是一门多领域交叉学科,涉及概率论、统计学、逼近论、凸分析、计算复杂性理论等多门学科。机器学习理论主要是设计和分析一些让计算机可以自动“学习”的算法。因为学习算法中涉及了大量的统计学理论,机器学习与推断统计学联系尤为密切,也被称为统计学习理论。算法设计方面,机器学习理论关注可以实现的,行之有效的学习算法。

参数技术

在数学和统计学裡,参数(英语:parameter)是使用通用变量来建立函数和变量之间关系(当这种关系很难用方程来阐述时)的一个数量。

LeNet技术

LeNet 诞生于 1994 年,是最早的卷积神经网络之一,并且推动了深度学习领域的发展。自从 1988 年开始,在许多次成功的迭代后,这项由 Yann LeCun 完成的开拓性成果被命名为 LeNet5。LeNet5 的架构基于这样的观点:(尤其是)图像的特征分布在整张图像上,以及带有可学习参数的卷积是一种用少量参数在多个位置上提取相似特征的有效方式。在那时候,没有 GPU 帮助训练,甚至 CPU 的速度也很慢。因此,能够保存参数以及计算过程是一个关键进展。这和将每个像素用作一个大型多层神经网络的单独输入相反。LeNet5 阐述了那些像素不应该被使用在第一层,因为图像具有很强的空间相关性,而使用图像中独立的像素作为不同的输入特征则利用不到这些相关性。

卷积神经网络技术

卷积神经网路(Convolutional Neural Network, CNN)是一种前馈神经网络,它的人工神经元可以响应一部分覆盖范围内的周围单元,对于大型图像处理有出色表现。卷积神经网路由一个或多个卷积层和顶端的全连通层(对应经典的神经网路)组成,同时也包括关联权重和池化层(pooling layer)。这一结构使得卷积神经网路能够利用输入数据的二维结构。与其他深度学习结构相比,卷积神经网路在图像和语音识别方面能够给出更好的结果。这一模型也可以使用反向传播算法进行训练。相比较其他深度、前馈神经网路,卷积神经网路需要考量的参数更少,使之成为一种颇具吸引力的深度学习结构。 卷积网络是一种专门用于处理具有已知的、网格状拓扑的数据的神经网络。例如时间序列数据,它可以被认为是以一定时间间隔采样的一维网格,又如图像数据,其可以被认为是二维像素网格。

映射技术

映射指的是具有某种特殊结构的函数,或泛指类函数思想的范畴论中的态射。 逻辑和图论中也有一些不太常规的用法。其数学定义为:两个非空集合A与B间存在着对应关系f,而且对于A中的每一个元素x,B中总有有唯一的一个元素y与它对应,就这种对应为从A到B的映射,记作f:A→B。其中,y称为元素x在映射f下的象,记作:y=f(x)。x称为y关于映射f的原象*。*集合A中所有元素的象的集合称为映射f的值域,记作f(A)。同样的,在机器学习中,映射就是输入与输出之间的对应关系。

监督学习技术

监督式学习(Supervised learning),是机器学习中的一个方法,可以由标记好的训练集中学到或建立一个模式(函数 / learning model),并依此模式推测新的实例。训练集是由一系列的训练范例组成,每个训练范例则由输入对象(通常是向量)和预期输出所组成。函数的输出可以是一个连续的值(称为回归分析),或是预测一个分类标签(称作分类)。

边缘检测技术

边缘检测是图像处理和计算机视觉中的基本问题,边缘检测的目的是标识数字图像中亮度变化明显的点。图像属性中的显著变化通常反映了属性的重要事件和变化。这些包括(i)深度上的不连续、(ii)表面方向不连续、(iii)物质属性变化和(iv)场景照明变化。 边缘检测是图像处理和计算机视觉中,尤其是特征检测中的一个研究领域。

图像处理技术

图像处理是指对图像进行分析、加工和处理,使其满足视觉、心理或其他要求的技术。 图像处理是信号处理在图像领域上的一个应用。 目前大多数的图像均是以数字形式存储,因而图像处理很多情况下指数字图像处理。

知乎机构

知乎作为中文互联网知名知识内容平台,致力于构建一个人人都可接入的知识分享网络,让人们便捷地与世界分享知识、经验和见解,高效获得可信赖的解答。 目前,知乎已经覆盖「问答」社区、一站式知识服务平台「知乎大学」、短内容分享功能「想法」等一系列产品和服务,并建立了包括音频、视频在内的多元媒介形式。截止 2018 年 8 月底,知乎用户数已突破 2 亿,回答数超过 1.2 亿。未来,知乎进一步加大对 AI 技术和应用的投入,构建一个由 AI 驱动的智能社区,让知识普惠每一个人。

https://www.zhihu.com
暂无评论
暂无评论~