网络结构的解析
- 拥有不同的层,不同的网络层有不同的功能
- 输入层:数据的预处理
- conv卷积层:卷积神经网路的核心网络层,用一组权重通过窗口滑动同时计算输入层
- relu 激活函数层,把卷积层的结果做非线性映射
- pool 池化层,压缩数据和参数的量,减少过拟合
- fc 全连接层。神经网络中的全连接层,通常在卷积神经网络的尾部,用来更好的拟合数据
一般的的卷积神经网络结构为:
$$input->[[conv->relu]*N]->poll] *M->[fc -> relus] *K$$
其中N,M,k代表了层结构的数量
上图中网络结构我们可以表示为:
$$ INPUT -> [[CONV-relu]1 -> POOL]2 -> [FC]*2$$
即是:$$N=1,M=2,K=2$$
我们看到卷积神经网络跟全连接神经网络不同之处在于:
- 卷积神经网络的神经元是按三维排列的,有用其深度,宽度,高度。
图说明:
从上面的图表示的神经网络中,
- 输入层的宽度跟高度跟图像的宽度跟高度对应,其深度为1
- 第一个卷积层对输入层的图像进行了卷积处理,得到3个feature map,'3个'因为这个卷积层包含了3个Filter(三个不同的权值矩阵),每个Filter都可以把输入层的数据卷积到一个Feature map. 通常Filter 也被称作通道(channel)
- 在第一个卷积层后,Pooling层对三个feature map做了一个下采样处理,得到了三个更小的feature map
- 然后到了第二层卷积层,5个filter 对上一层的三个Feature map处理得到新的5个Feature Map,接着继续进行Pooling层,得到5个更小的Feature Map,然后就是第二个Pooling层,进行下采样,,这样得到5个更小的Feature Map
- 最后两层是全连接层,第一个全连接层的每个神经元,和上一层的5个Feature Map中的每个神经元连接,第二个全连接层(输出层),则与第一个全连接层的神经元相连,这样得到整个网络的输出
一、输入层:
输入层一般进行数据的处理
-
去均值:
如图:
左边是原始数据,右边是去均值后的数据
它将数据中心整体移动到0点,方便做计算(做求导计算的时候减少计算量) 归一化:
归一化是为了减少计算范围,避免某一个特征维在计算的时候比重过大(在决策树中使用过)
归一化公式:$$a=a/(max-min)$$
它将所有维的特征值的变化都缩小到0-1的范围内
二、Relu层(激活层)
之所以叫relu层是因为这个层是采用Relu函数作为激活函数,对卷积层的输出进行激活处理,一般跟Relu层跟卷积层合在一层叫做卷积层.
Relu函数的定义是:
$$F(x)=max(0,x)$$
它的导数是分段的:
$f'(x)=0 ,x<0$
$f'(x)=1 ,x>0$
当然,你也可以选择其他函数做为卷积层的激励函数:
比如:
- Sigmid
- Tanh
- Leaky Relu
三、卷积层的解析
(1)卷积的计算
假设有一个5*5的图像,使用一个3*3的filter进行卷积,想得到一个3*3的Feature Map,如下所示:
我们对矩阵的元素进行标记:
(即在第0,0格中,3*3的窗口固定在image矩阵的左上角位置,3*3的矩阵与3*3的Filter矩阵对应位置相乘然后取和,然后其激活函数的输出作为输出矩阵的第0,0格的值)
如:
第二个位置时候(A0,1):
3*3的窗口往右移动一个步长(strike,这个例子步长为1):然后依次对应位置相乘取和,再通过激活函数,得到A0,1的值:
下面的动图展示了Feature Map的每个位置的计算:
上面的情况是步长为1的计算过程,事实上,步长就是窗口(与Filter同样大小的)每次移动的距离,当步长为2的时候:
我们看到Feature Map的大小跟步长的设定有关系:
- W2是卷积后Feature Map的宽度,W1是卷积前的图像宽度,F是Filter的宽度,P是Zero Pading的数量(指在原始图像周围补几圈0),S是步长,
- H同理是其高度表示
当卷积层的深度大于1,即多个Filter叠在一起同时计算时,只要把深度那一维也同时进行对应位置相乘然后加到之前的和里面经过激励函数输出即可,计算公式为:
下面的动图介绍了RGB三维图像经过2个3*3*3的Filter后计算过程:
这里体现了一些卷积神经网络的特征:
- 权重共享 Filter的权值对于上一层的所有神经元都是一样的
- 局部相连:每一层的神经元只与上一层的部分神经元相连
对于两个3*3*3的卷积层来说,其参数数量仅有(3*3*3+1)*2=56个,且参数数量与上一层神经元个数无关.
卷积公式表示卷积层
数学中的二维卷积公式为:
我们可以将其表示为:
$$C=A*B$$
我们如果按上面公式计算,其实会发现A其实是Filter,而矩阵B是待输入的矩阵,而位置关系也不同:
卷积层中的"卷积"看上去是将input矩阵旋转了180°进行计算然后把AB位置调换进行的卷积计算,这种计算其实叫做'互相关'
如果我们不考虑这些小的差别的话.步长为1的卷积层计算我们可以简化公式为:
四、Pooling层
Pooling层也叫池化层或者下采样层,它将卷积层得到的输出进行采集"有用"数据并抽象化,从而减少参数减少计算量.
池化方法常用的有:Max Pooling 跟Mean Pooling两种,
前者求窗口最大值作为池化后的该格子的输出值,后者取平均值作为格子输出值
Pooling 层的深度跟卷积层数深度一样,所以是卷积的各层Feature Map分别做Pooling处理
全连接层
全连接层的输出值计算与训练与上篇一样
训练与数学推导(数学公式重灾区)
整个卷积神经网络的训练我们都采用反向传播(Back Propagatio)算法进行训练,算法 的介绍为:
其中损失函数$Ed$的定义为:
取网络所有输出层节点的误差平方作为损失函数:
卷积误差项的传递
我们先来考虑步长为1,输入深度为1,Filter个数为1的最简单情况:
假设输入的大小为3*3,filter大小为2*2,按步长为1卷积,我们将得到2*2的feature map。如下图所示:
根据链式法则:
我们发现,计算
如图:
因为数学意义的[卷积]和互相关操作是可以转化的。首先,我们把矩阵A翻转180度,然后再交换A和B的位置(即把B放在左边而把A放在右边。卷积满足交换率,这个操作不会导致结果变化),那么卷积就变成了互相关。
那么,上面的计算可以用卷积公式表示:
Wl代表了第l层的Filter的权重矩阵,
和的形式:
:
所以只要对激活函数Relu求导即可:
所以两项相乘:
改写成卷积形式:
代表了互相关操作.
这样步长为1、输入的深度为1、filter个数为1的最简单的情况,卷积层误差项传递的算法。
我们可以看出,因为步长为2,得到的feature map跳过了步长为1时相应的部分。
Filter数量为N时候的误差传递:
D为深度
卷积Filter权重梯度的计算:
计算
由于是权值共享,权值
计算:
所以其公式是:
偏置项的梯度:
所有sensivity map所有的误差项之和.
对于步长为S的卷积层,处理方法跟传递误差项是一样的:
- 将sensitivity map还原成步长为1的sensitivity map,
- 再用上面的方法计算
获得了所有的梯度之后,根据梯度下降算法更新每个权重.
梯度下降,简单来说就是:
$$w=w-f'(w)$$
这样,我们就解决了卷积层的训练
Pooling层的训练:
max pooling 跟Mean pooling都没有参数,所以需要将误差项传递到上一层
max Pooling层
所以得:
所以,对于max pooling,下一层的误差项的值会原封不动的传递到上一层区块的最大值对应的神经元,而其他神经元的误差项的值都是0
mean Pooling层
所以对于mean Pooling层,下一层的误差项会平均分配到上一层所对应 的区块中.
全连接层
参看 神经网络这篇博客的介绍:
代码实现:
导入工具包:
import numpy as np
卷积层的初始化:
class ConvLayer(object):
def __init__(self,input_width,input_height
,channel_number,filter_width
,filter_height,filter_number
,zero_padding,stride,activator,
learning_rate):
'''
:param input_width: 输入矩阵的宽
:param input_height:输入矩阵的高
:param channel_number:
:param filter_width:共享权重的filter矩阵宽
:param zero_padding:补几圈0
:param stride:窗口每次移动的步长
:param activator:激励函数
:param learning_rate:学习率
:param filter_height共享权重的filter矩阵宽
:param filter_number filter的深度
'''
self.input_width = input_height
self.input_height = input_height
self.channel_number = channel_number
self.filter_width = filter_width
self.filter_height = filter_height
self.filter_number = filter_number
self.zero_padding = zero_padding
self.stride = stride
self.activator = activator
self.learning_rate = learning_rate
self.output_height = ConvLayer.calculate_output_size(
self.input_height,filter_height,zero_padding,stride
)
self.output_width = ConvLayer.calculate_output_size(
self.input_width,self.filter_width,zero_padding,stride
)
self.output_array = np.zeros((self.filter_number,
self.output_height,
self.output_width))
self.filters = []
for i in range(filter_number):
self.filters.append(Filter(filter_width,
filter_height,
self.channel_number))
calculate_output_size函数用来计算卷积层输出:
@staticmethod
def calculate_output_size(input_size,filter_size
,zero_padding,stride):
return int( (input_size - filter_size + 2 * zero_padding) / stride + 1)
定义Filter类与Relu激活函数:
class Filter(object):
#Filter 类 保存了卷积层的参数以及梯度,并用梯度下降的办法更新参数
#权重随机初始化为一个很小的值,而偏置项初始化为0。
def __init__(self,width,height,depth):
self.weights = np.random.uniform(-1e-4,1e-4,(depth,height,width))
self.bias =0
self.weights_grad = np.zeros(self.weights.shape)
self.bias_grad = 0
def __repr__(self):
return 'filter weights:\n%s\nbias:\n%s' % (
repr(self.weights), repr(self.bias))
def get_weights(self):
return self.weights
def get_bias(self):
return self.bias
def update(self,learning_rate):
self.weights -= learning_rate * self.weights_grad
self.bias -= learning_rate * self.bias_grad
class ReluActivator(object):
def forward(self,weighted_input):
return max(0,weighted_input)
def backward(self,output):
return 1 if output > 0 else 0
卷积层前计算:
def forward(self,input_array):
'''
计算卷积层的输出
:param input_array: 前一层的输出
:return: 没有返回,输出结果保存到self.output_array
'''
self.input_array = input_array
self.padded_input_array = padding(input_array,self.zero_padding)
for f in range(self.filter_number):
filter = self.filters[f]
conv(self.padded_input_array,filter.get_weights(),
self.output_array[f],self.stride,filter.get_bias())
element_wise_op(self.output_array,self.activator.forward)
前计算用到的 工具函数:
def padding(input_array, zp):
'''
将输入矩阵补0
:param input_array:
:param zp: 补0的圈数
:return:
python3 玄学除法,int 变float
'''
zp = int(zp)
if zp ==0:
return input_array
else:
if input_array.ndim==3:
input_width = input_array.shape[2]
input_height = input_array.shape[1]
input_depth = input_array.shape[0]
padder_array = np.zeros((input_depth,input_height+2*zp,input_width+2*zp))
padder_array[:,zp:zp+input_height,zp:zp+input_width]=input_array
return padder_array
elif input_array.ndim==2:
input_height = input_array.shape[0]
input_width = input_array.shape[1]
padder_array = np.zeros((input_height+2*zp,input_width+2*zp))
padder_array[zp:zp+input_height,zp:zp+input_width]=input_array
return padder_array
def element_wise_op(array, op):
'''
对numpy数组元素依次进行op操作(这里是函数)
:param array:
:param op:
:return:
'''
for i in np.nditer(array,
op_flags=['readwrite']):
i[...] = op(i)
def conv(input_array,kernel_array,output_array,stride,bias):
'''
计算卷积
:param input_array:
:param kernel_array:
:param output_array:
:param stride:
:param bias:
:return:
'''
channel_number = input_array.ndim
output_width = output_array.shape[1]
output_height = output_array.shape[0]
kernel_width = kernel_array.shape[-1]
kernel_height = kernel_array.shape[-2]
for i in range(output_height):
for j in range(output_width):
#依次计算每一格的卷积
output_array[i][j] = (get_patch(input_array,i,j,
kernel_width,kernel_height,stride) * kernel_array ).sum()+bias
def get_patch(input_array, i, j, kernel_width,
kernel_height, stride):
'''
获得窗口移动后input的array
'''
i*=stride
j*=stride
max_height = i + kernel_height
max_width = j + kernel_width
if input_array.ndim == 3:
max_z = input_array.shape[0] + 1
return input_array[0:max_z, i:max_height, j:max_width]
else:
return input_array[i:max_height, j:max_width]
卷积层的反向传播代码,这些方法都在ConvLayer类中:
def bp_sensitivity_map(self,sensitivity_array,activator):
'''
卷积层反向传播算法的实现
1,将误差项传递到上一层
2:计算每个参数的梯度
3:更新参数
:param sensitivity_array: 本层的sensitivity map
:param activator: 上一层的激活函数
:return:
'''
expanded_array = self.expand_sensitivity_map(sensitivity_array)
#full 卷积
expanded_width = expanded_array.shape[2]
#获得补0 数
zp = (self.input_width+self.filter_width-1-expanded_width)/2
padded_array = padding(expanded_array,zp)
#创建初始误差矩阵
self.delta_array = self.create_delta_array()
#对于具有多个filter的卷积层来说,最终传递到上一层的sensitivity map
#相当于把所有的filter的sensitivity map之和
for f in range(self.filter_number):
filter = self.filters[f]
# 将filter权重翻转180度
flipped_weights = np.array(map(
lambda i: np.rot90(i, 2),
filter.get_weights()))
#python3运行的时候这个map有问题
#计算每一个filter的delta_array
delta_array = self.create_delta_array()
for d in range(delta_array.shape[0]):
conv(padded_array[f],flipped_weights[d],delta_array[d],1,0)
self.delta_array+=delta_array
#创建激活函数矩阵(卷积反向传播误差项的第二项)
derivative_array = np.array(self.input_array)
element_wise_op(derivative_array,self.activator.backward)
self.delta_array *= derivative_array
def bp_gradient(self,sensitivity_array):
'''
计算梯度,包括权重跟偏置项
:param sensitivity_array:
:return:
'''
expanded_array = self.expand_sensitivity_map(sensitivity_array)
for f in range(self.filter_number):
filter = self.filters[f]
for d in range(filter.get_weights().shape[0]):
conv(self.padded_input_array[d],expanded_array[f],
filter.weights_grad[d],1,0)
filter.bias_grad = expanded_array[f].sum()
def expand_sensitivity_map(self,sensitivity_array):
'''
将步长为S的map 还原成步长1的map
:param sensitivity_array:
:return:
'''
expanded_depth = sensitivity_array.shape[0]
expanded_height = (self.input_height-self.filter_height+2*self.zero_padding+1)
expanded_width = (self.input_width-self.filter_width+2*self.zero_padding+1)
expanded_array = np.zeros((expanded_depth,expanded_height,expanded_width))
for i in range(self.output_height):
for j in range(self.output_width):
i_pos = i * self.stride
j_pos = j * self.stride
expanded_array[:,i_pos,j_pos]=sensitivity_array[:,i,j]
return expanded_array
def create_delta_array(self):
return np.zeros((self.channel_number,self.input_height,self.input_width))
def update(self):
'''
更新这一层的权重跟偏置项,很简单依次更新每一个filter就行了
:return:
'''
for filter in self.filters:
filter.update(self.learning_rate)
def backward(self, sensitivity_array, activator=None):
if not activator:
activator = self.activator
self.bp_sensitivity_map(sensitivity_array, activator)
self.bp_gradient(sensitivity_array)
为了验证我们的卷积层是否写得正确,验证代码:
from conv import *
#conv是我卷积层类所在的python文件,此处导入模块
import numpy as np
class IdentityActivator(object):
def forward(self, weighted_input):
#return weighted_input
return weighted_input
def backward(self, output):
return 1
def init_test():
a = np.array(
[[[0,1,1,0,2],
[2,2,2,2,1],
[1,0,0,2,0],
[0,1,1,0,0],
[1,2,0,0,2]],
[[1,0,2,2,0],
[0,0,0,2,0],
[1,2,1,2,1],
[1,0,0,0,0],
[1,2,1,1,1]],
[[2,1,2,0,0],
[1,0,0,1,0],
[0,2,1,0,1],
[0,1,2,2,2],
[2,1,0,0,1]]])
b = np.array(
[[[0,1,1],
[2,2,2],
[1,0,0]],
[[1,0,2],
[0,0,0],
[1,2,1]]])
cl = ConvLayer(5,5,3,3,3,2,1,2,IdentityActivator(),0.001)
cl.filters[0].weights = np.array(
[[[-1,1,0],
[0,1,0],
[0,1,1]],
[[-1,-1,0],
[0,0,0],
[0,-1,0]],
[[0,0,-1],
[0,1,0],
[1,-1,-1]]], dtype=np.float64)
cl.filters[0].bias=1
cl.filters[1].weights = np.array(
[[[1,1,-1],
[-1,-1,1],
[0,-1,1]],
[[0,1,0],
[-1,0,-1],
[-1,1,0]],
[[-1,0,0],
[-1,0,1],
[-1,0,0]]], dtype=np.float64)
return a, b, cl
def gradient_check():
'''
梯度检查
'''
# 设计一个误差函数,取所有节点输出项之和
error_function = lambda o: o.sum()
# 计算forward值
a, b, cl = init_test()
cl.forward(a)
# 求取sensitivity map,是一个全1数组
sensitivity_array = np.ones(cl.output_array.shape,
dtype=np.float64)
# 计算梯度
cl.backward(sensitivity_array,
IdentityActivator())
cl.update()
# 检查梯度
epsilon = 10e-4
for d in range(cl.filters[0].weights_grad.shape[0]):
for i in range(cl.filters[0].weights_grad.shape[1]):
for j in range(cl.filters[0].weights_grad.shape[2]):
cl.filters[0].weights[d,i,j] += epsilon
cl.forward(a)
err1 = error_function(cl.output_array)
cl.filters[0].weights[d,i,j] -= 2*epsilon
cl.forward(a)
err2 = error_function(cl.output_array)
expect_grad = (err1 - err2) / (2 * epsilon)
cl.filters[0].weights[d,i,j] += epsilon
print('weights(%d,%d,%d): expected - actural %f - %f' % (
d, i, j, expect_grad, cl.filters[0].weights_grad[d,i,j]))
gradient_check()
Max Pooling 层的实现:
# -*- coding:utf-8 -*-
#!/usr/bin/local/bin/python
import numpy as np
from tools import *
class MaxPoolingLayer(object):
def __init__(self,input_width,input_height,
channel_number,filter_width,
filter_height,stride):
self.input_width = input_width
self.input_height = input_height
self.channel_number = channel_number
self.filter_width = filter_width
self.filter_height = filter_height
self.stride = stride
self.output_width = (input_width-filter_width)/self.stride + 1
self.output_height = (input_height - filter_height)/self.stride +1
self.output_array = np.zeros((self.channel_number,
self.output_height,self.output_width))
def forward(self,input_array):
self.input_array = input_array
for d in range(self.channel_number):
for i in range(self.output_height):
for j in range(self.output_width):
self.output_array[d,i,j] = (get_patch(
input_array[d],i,j,
self.filter_width,
self.filter_height,
self.stride
).max())
def backward(self,sensitivity_array):
self.delta_array = np.zeros(self.input_array.shape)
for d in range(self.channel_number):
for i in range(self.output_height):
for j in range(self.output_width):
patch_array = get_patch(
self.input_array[d],i,j,
self.filter_width,
self.filter_height,
self.stride
)
k,l =get_max_index(patch_array)
self.delta_array[d,
i*self.stride+k,
j*self.stride+l] = sensitivity_array[d,i,j]
def update(self):
#因为不需要进行更新权重,所以此方法pass,但是为了保证整个网络更新的时候可以用layers.update()方法统一更新权值,所以写了个空方法
pass
tools文件定义了两个工具方法:
# -*- coding:utf-8 -*-
import numpy as np
def get_patch(input_array, i, j, kernel_width,
kernel_height, stride):
'''
获得窗口移动后input的array
'''
i*=stride
j*=stride
max_height = i + kernel_height
max_width = j + kernel_width
if input_array.ndim == 3:
max_z = input_array.shape[0] + 1
return input_array[0:max_z, i:max_height, j:max_width]
else:
return input_array[i:max_height, j:max_width]
def get_max_index(arr):
'''
获取数组中的最大值,返回坐标
:param arr:
:return:
'''
idx = np.argmax(arr)
return (int(idx / arr.shape[1]), idx % arr.shape[1])
全连接层 fc.py:
# -*- coding:utf-8 -*-
#!/usr/bin/local/bin/python
import numpy as np
class FullConnectedLayer(object):
def __init__(self,input_size,
output_size,
learing_rate,
activator):
self.input_size = input_size
self.output_size = output_size
self.activator = activator
self.learning_rate = learing_rate
self.W = np.random.uniform(-0.1,0.1,(output_size,input_size))
self.b = np.zeros((output_size,1))
self.output = np.zeros((output_size,1))
def forward(self,input_array):
self.input = input_array
self.output = self.activator.forward(
np.dot(self.W,input_array)+self.b
)
def backward(self,delta_array):
self.delta = self.activator.backward(self.input) * np.dot(
self.W.T,delta_array
)
self.W_grad = np.dot(delta_array,self.input.T)
self.b_grad = delta_array
def update(self):
self.W += self.learning_rate * self.W_grad
self.b += self.learning_rate * self.b_grad
这里我们完成了卷积层,pooling(池化)层,全连接层的定义,我们就可以用这些简陋的轮子搭建一个简单的卷积神经网络了.
为什么写这些代码而不用框架:
- 框架是别人写好的轮子,只会用框架不能理解其中的含义
- 自己写的代码更容易理解其中的算法与数学含义
- 个人建议,学习阶段写轮子,使用阶段用别人的轮子
我用上面的代码搭建了一个巨简陋的卷积神经网络去试试MNIST手写数字数据集的识别
完整代码在:
git clone https://github.com/coldsummerday/mllearn.git
cd mllearn-master/cnn/
#建议用python2,用python3的朋友吧conv文件中的bp_sensitivity_map中有个map方法改成python3的即可
python main.py
#good luck!