前言
用输入层,隐藏层,输出层组成了神经网络,把数据放入输入层,通过隐藏层,再到输出层,把训练的数据跟输出进行对比得出误差,把误差传回到隐藏层中训练各个层的参数。
这是典型的神经网络的结果图:
典型的神经网络用在了很多的场合中,比如分类上,也达到了很好的效果。
但是如果输入层的参数太多,会出现怎样的一种情况?如下是用神经网络来识别手写数字:
这是一个神经网络中常见的一个应用,如何用神经网络进行数字的识别?
最简单的一种想法就是,把图片的每个像素作为一个输入点,如果一张手写数字为32x32像素,输入节点为32x32=1024个,隐藏层的数目假设和输入层一样32x32=1024个,输出层为10个(代表从0到9的数字),这里总共有几个参数需要训练呢?1024x1024 + 1024x10=1058816,这样的参数是可怕的。
大脑对于图片的认知过程,是逐层的迭代和抽象的过程,从像素感知到抽取图片框架,大脑也是逐层感知。在进行这个过程时并不需要这么多的数据,对于机器来说,需要的是图片的重要信息,通过逐层的抽取,提出图片的特征,并用训练BP神经 网络。这种方法叫做卷积神经网络。
98年大名鼎鼎的Yann Lecun,提出了卷积神经网络算,并应用在手写数字的识别上。
卷积神经网络
卷积神经网络的大部分思想跟BP神经网络是一样的,也可以看做是对BP神经网络的一种扩展和改进,目的是降低训练参数的个数。在卷积神经网络中如何降低训练参数的呢?主要是在BP神经网络的基础上引进了两层特殊层,一个叫做卷积层,一个叫做池化层。
这个卷积的过程如下:
卷积的过程:卷积->池化(采样)->卷积->池化(采样)->全连接层。
卷积层
卷积层是卷积核在上一级输入层上通过逐一滑动窗口计算而得,卷积核中的每一个参数都相当于传统神经网络中的权值参数,与对应的局部像素相连接,将卷积核的各个参数与对应的局部像素值相乘之和,(通常还要再加上一个偏置参数),得到卷积层上的结果。
为简便起见,考虑一个大小为5×5的图像,和一个3×3的卷积核。这里的卷积核共有9个参数,就记为 Θ=[θij]3×3 吧。这种情况下,卷积核实际上有9个神经元,他们的输出又组成一个3×3的矩阵,称为特征图。第一个神经元连接到图像的第一个3×3的局部,第二个神经元则连接到第二个局部。具体如下图所示。
通过卷积定义,可以简单的认为是原始图片的数据和卷积核的乘机再求和,就是对图片的卷积操作。
图片通过卷积层后,加上一个偏置,还需要做激励运算。
卷积层的前向过程,总结出来如下:
def propagate(self):
FMs = np.zeros([self.currLayer.get_n(), self.currLayer.shape()[0], self.currLayer.shape()[1]])
inFMs = self.prevLayer.get_FM()
k = 0 # kernel index, there is one foreach i, j combination
for j in range(self.currLayer.get_n()): # foreach FM in the current layer
for i in range(self.prevLayer.get_n()): # foreach FM in the previous layer
if self.connections[i, j] == 1:
# foreach neuron in the feature map
for y_out in range(self.currLayer.shape()[0]):
for x_out in range(self.currLayer.shape()[1]):
# iterate inside the visual field for that neuron
for y_k in range(0, self.kernelHeight, self.stepY):
for x_k in range(0, self.kernelWidth, self.stepX):
FMs[j, y_out, x_out] += inFMs[i, y_out + y_k, x_out + x_k] * self.k[k, y_k, x_k]
# add bias
FMs[j, y_out, x_out] += 1 * self.biasWeights[j]
# next kernel
k += 1
# compute sigmoid (of a matrix since it's faster than elementwise)
FMs[j] = self.act.func(FMs[j])
#print "out = ", FMs
self.currLayer.set_FM(FMs)
return FMs
回到原来的那张总图中,原始图像进来以后,先进入一个卷积层C1,由6个5x5的卷积核组成,卷积出28x28(28是这样定义的:32-5+1)的图像,C1有156个可训练参数(每个滤波器55=25个unit参数和一个bias参数,一共6个滤波器,共(55+1)6=156个参数),注意这里每个卷积核只有55=25个参数,而不是没卷积一次的参数都要变。
池化层
池化(pool)即下采样(downsamples),目的是为了减少特征图。池化操作对每个深度切片独立,规模一般为 2*2,相对于卷积层进行卷积运算,池化层进行的运算一般有以下几种:
- 最大池化(Max Pooling)。取4个点的最大值。这是最常用的池化方法。
- 均值池化(Mean Pooling)。取4个点的均值。
- 高斯池化。借鉴高斯模糊的方法。不常用。
- 可训练池化。训练函数 ff ,接受4个点为输入,出入1个点。不常用。
最常见的池化层是规模为2*2, 步幅为2,对输入的每个深度切片进行下采样。每个MAX操作对四个数进行,如下图所示:
采用最大值的采用方式:
def propagate(self):
[prevSizeY, prevSizeX] = self.prevLayer.shape()
[currSizeY, currSizeX] = self.currLayer.shape()
self.maximaLocationsX = np.zeros([self.currLayer.get_n(), self.currLayer.shape()[0], self.currLayer.shape()[1]])
self.maximaLocationsY = np.zeros([self.currLayer.get_n(), self.currLayer.shape()[0], self.currLayer.shape()[1]])
pooledFM = np.zeros([self.currLayer.get_n(), self.currLayer.shape()[0], self.currLayer.shape()[1]])
yi = self.prevLayer.get_FM()
for n in range(self.prevLayer.get_n()):
for i in range(currSizeY):
for j in range(currSizeX):
reg = yi[n, i*self.poolingStepY:(i+1)*self.poolingStepY, j*self.poolingStepX:(j+1)*self.poolingStepX]
loc = np.unravel_index(reg.argmax(), reg.shape) + np.array([i*self.poolingStepY, j*self.poolingStepY])
self.maximaLocationsY[n, i, j] = loc[0]
self.maximaLocationsX[n, i, j] = loc[1]
pooledFM[n, i, j] = yi[n, loc[0], loc[1]]
self.currLayer.set_FM(pooledFM)
通过池化层的图像数据进一步的减少了,维数进一步减少。
通过了卷积层,池化层,还得继续在通过卷积层,池化层,只是里面的参数不同而已,训练的方式是一样的。
全连接层
这一层更BP神经网络并没有区别,主要是把训练的参数变成结果。
这层基本上将一个输入量(无论输出是卷积或ReLU或池层)和输出一个N是程序选择类别的N维向量,这个全连接层的工作方式是,它着眼于前一层的输出(代表高阶特征的激活图),并确定哪些功能是最相关特定的类。例如如果该程序预测,一些图像是一只狗,它在激活图中会有高的值,代表高阶特征如一个爪子或4条腿等。类似地,如果该程序是预测一些图像是鸟的功能,它在激活图中会有高阶值,代表高阶特征如如翅膀或喙等。
计算输出结果:
def propagate(self):
x = self.prevLayer.get_x()[np.newaxis]
if self.currLayer.hasBias:
x = np.append(x, [1])
z = np.dot(self.w.T, x)
# compute and store output
y = self.act.func(z)
self.currLayer.set_x(y)
return y
整个过程可以用如下的方式进行组织:
inputLayer0 = layerFM(1, 32, 32, isInput = True)
convLayer1 = layerFM(6, 28, 28)
poolLayer2 = layerFM(6, 14, 14)
convLayer3 = layerFM(16, 10, 10)
poolLayer4 = layerFM(16, 5, 5)
convLayer5 = layerFM(100, 1, 1)
hiddenLayer6 = layer1D(80)
outputLayer7 = layer1D(10, isOutput = True)
convolution01 = convolutionalConnection(inputLayer0, convLayer1, np.ones([1, 6]), 5, 5, 1, 1)
pooling12 = poolingConnection(convLayer1, poolLayer2, 2, 2)
convolution23 = convolutionalConnection(poolLayer2, convLayer3, np.ones([6, 16]), 5, 5, 1, 1)
pooling34 = poolingConnection(convLayer3, poolLayer4, 2, 2)
convolution45 = convolutionalConnection(poolLayer4, convLayer5, np.ones([16, 100]), 5, 5, 1, 1)
full56 = fullConnection(convLayer5, hiddenLayer6)
full67 = fullConnection(hiddenLayer6, outputLayer7)
训练过程
卷积神经网络训练参数的方法,也是采用反向传播,跟BP神经网络一样。通过误差向前传播的方式,不断的调整需要训练的参数。
全连层的误差计算和权重调整方式:
def bprop(self, ni, target = None, verbose = False):
yj = self.currLayer.get_x()
if verbose:
print "out = ", yj
print "w = ", self.w
# compute or retreive error of current layer
if self.currLayer.isOutput:
if target is None: raise Exception("bprop(): target values needed for output layer")
currErr = -(target - yj) * self.act.deriv(yj)
self.currLayer.set_error(currErr)
else:
currErr = self.currLayer.get_error()
if verbose: print "currErr = ", currErr
yi = np.append(self.prevLayer.get_x(), [1])
# compute error of previous layer
if not self.prevLayer.isInput:
prevErr = np.zeros(len(yi))
for i in range(len(yi)):
prevErr[i] = sum(currErr * self.w[i]) * self.act.deriv(yi[i])
self.prevLayer.set_error(np.delete(prevErr,-1))
# compute weight updates
dw = np.dot(np.array(yi)[np.newaxis].T, np.array(currErr)[np.newaxis])
self.w -= ni * dw
池化层的误差计算方法:
def bprop(self):
currErr = self.currLayer.get_FM_error()
prevErr = np.zeros([self.prevLayer.get_n(), self.prevLayer.shape()[0], self.prevLayer.shape()[1]])
[currSizeY, currSizeX] = self.currLayer.shape()
for n in range(self.prevLayer.get_n()):
for i in range(currSizeY):
for j in range(currSizeX):
prevErr[n, self.maximaLocationsY[n, i, j], self.maximaLocationsX[n, i, j]] = currErr[n, i, j]
self.prevLayer.set_FM_error(prevErr)
卷积层的误差和权重调整:
def bprop(self, ni, target = None, verbose = False):
yi = self.prevLayer.get_FM() # get output of previous layer
yj = self.currLayer.get_FM() # get output of current layer
# TODO: A conv. layer cannot be an output, remove computing error part
if not self.currLayer.isOutput:
currErr = self.currLayer.get_FM_error()
else:
currErr = -(target - yj) * self.act.deriv(yj)
self.currLayer.set_FM_error(currErr)
#print "\ncurrent error = \n", currErr
# compute error in previous layer
prevErr = np.zeros([self.prevLayer.get_n(), self.prevLayer.shape()[0], self.prevLayer.shape()[1]])
biasErr = np.zeros([self.currLayer.get_n()])
k = 0
for j in range(self.currLayer.get_n()): # foreach FM in the current layer
for i in range(self.prevLayer.get_n()): # foreach FM in the previous layer
if self.connections[i, j] == 1:
# foreach neuron in the feature map
for y_out in range(self.currLayer.shape()[0]):
for x_out in range(self.currLayer.shape()[1]):
# iterate inside the visual field for that neuron
for y_k in range(0, self.kernelHeight, self.stepY):
for x_k in range(0, self.kernelWidth, self.stepX):
#FMs[j, y_out, x_out] += inFMs[i, y_out + y_k, x_out + x_k] * self.k[k, y_k, x_k]dd
prevErr[i, y_out + y_k, x_out + x_k] += self.k[k, y_k, x_k] * currErr[j, y_out, x_out]
# add bias
biasErr[j] += currErr[j, y_out, x_out] * self.k[k, y_k, x_k]
# next kernel
k += 1
for i in range(self.prevLayer.get_n()):
prevErr[i] = prevErr[i] * self.act.deriv(yi[i])
for j in range(self.currLayer.get_n()):
biasErr[j] = biasErr[j] * self.act.deriv(1)
self.prevLayer.set_FM_error(prevErr)
# compute weights update
dw = np.zeros(self.k.shape)
dwBias = np.zeros(self.currLayer.get_n())
k = 0
for j in range(self.currLayer.get_n()): # foreach FM in the current layer
for i in range(self.prevLayer.get_n()): # foreach FM in the previous layer
if self.connections[i, j] == 1:
# foreach neuron in the feature map
for y_out in range(self.currLayer.shape()[0]):
for x_out in range(self.currLayer.shape()[1]):
# iterate inside the visual field for that neuron
for y_k in range(0, self.kernelHeight, self.stepY):
for x_k in range(0, self.kernelWidth, self.stepX):
#FMs[j, y_out, x_out] += inFMs[i, y_out + y_k, x_out + x_k] * self.k[k, y_k, x_k]dd
dw[k, y_k, x_k] += yi[i, y_out + y_k, x_out + x_k] * currErr[j, y_out, x_out]
# add bias
dwBias[j] += 1 * currErr[j, y_out, x_out]
# next kernel
k += 1
# update weights
self.k -= ni * dw
self.biasWeights -= ni * dwBias
总结
卷积神经网络是用的较多的一种网络,通过卷积和池化减少了大量的训练参数,同时又能够快速缩减误差,能达到训练网络的目标。卷积神经网络大量的应用到图片的分类上,也取得了很好的效果,它是一种BP神经网络的改进和扩展,使得BP神经网络能够用在大输入的数据上。