pytorch实现:
class LeNet(BasicModule):
def __init__(self, num_classes, inChannels=3, use_ReLU=True):
super(LeNet, self).__init__()
self.model_name = 'lenet'
if use_ReLU:
self.features = nn.Sequential(
nn.Conv2d(inChannels, 6, 5),
nn.ReLU(),
nn.MaxPool2d(2, 2),
nn.Conv2d(6, 16, 5),
nn.ReLU(),
nn.MaxPool2d(2, 2)
)
else:
self.features = nn.Sequential(
nn.Conv2d(inChannels, 6, 5),
nn.Sigmoid(),
nn.MaxPool2d(2, 2),
nn.Conv2d(6, 16, 5),
nn.Sigmoid(),
nn.MaxPool2d(2, 2)
)
self.classifier = nn.Sequential(
nn.Linear(16 * 5 * 5, 120),
nn.ReLU(),
nn.Linear(120, 84),
nn.ReLU(),
nn.Linear(84, num_classes)
)
def forward(self, x):
x = self.features(x)
x = x.view(-1, 16 * 5 * 5)
x = self.classifier(x)
return x
5x5conv1 Nx32x32xC->Nx28x28x6
2x2 max pooling1 Nx28x28x6->Nx14x14x6
5x5 conv2 Nx14x14x6->Nx10x10x16
2x2 max pooling2 Nx10x10x16->Nx5x5x16
fully connection1 Nx5x5x16->Nx400x120
fully connection2 Nx400x120->Nx120x84
fully connection3 Nx120x84->Nx84xnclasses
LeNet: 卷积神经网络开山之作.
主要创新点
- 局部感受野(local receptive fields):
- 权值共享(shared weights): 大大降低了参数的数量。
- 下采样(sub-sampling): 有效的降低输出对尺度和形变的敏感性。
重点概念
- 卷积
参考: https://blog.csdn.net/wweiainn/article/details/80231792
tensorflow实现卷积有两种方式:
a. 全零填充(padding=SAME):
在卷积计算中保持输入特征图的尺寸不变,可以使用全零填充,在输入特征图周围填充0. 使用全零填充(padding=SAME):
输出特征图边长 = 输入特征图边长 / 步长(向上取整)
b. 不使用(padding=VALID):
输出特征图边长 = (输入特征图边长 - 核长 + 1 )/ 步长(向上取整)
先看一下卷积实现原理,对于in_c个通道的输入图,如果需要经过卷积后输出out_c个通道图,那么总共需要in_c * out_c个卷积核参与运算
如图所示:
假如输入temsor为 1x5x5x4, 表示输入batch=1, h, w, c分别为5,5,4, 假设padding=1,方式为same, 卷积核tensor为: 3x3x4x3, 表示3x3大小的卷积核, 输出通道数为3, 则最终输出的feature map tensor为:1x5x5x3. 对于单个输出通道中的每个点,取值为对应的一组4个不同的卷积核经过卷积计算后的和.
实例演示:
#输入,shape=[c,h,w]
input_data=[
[[1,0,1,2,1],
[0,2,1,0,1],
[1,1,0,2,0],
[2,2,1,1,0],
[2,0,1,2,0]],
[[2,0,2,1,1],
[0,1,0,0,2],
[1,0,0,2,1],
[1,1,2,1,0],
[1,0,1,1,1]],
]
#卷积核,shape=[in_c,k,k]=[2,3,3]
weights_data=[
[[ 1, 0, 1],
[-1, 1, 0],
[ 0,-1, 0]],
[[-1, 0, 1],
[ 0, 0, 1],
[ 1, 1, 1]]
]
比如现在输入input_data: 2x5x5的numpy, 卷积核为3x3的numpy, 假如想输出通道数为1, 则 需要的卷积核为3x3x2x1.因为tensorflow的tensor通道顺序和numpy有所差别, 所以要先进行通道顺序变换. tensorflow的卷积过程为:
import tensorflow as tf
import numpy as np
input_data=[
[[1,0,1,2,1],
[0,2,1,0,1],
[1,1,0,2,0],
[2,2,1,1,0],
[2,0,1,2,0]],
[[2,0,2,1,1],
[0,1,0,0,2],
[1,0,0,2,1],
[1,1,2,1,0],
[1,0,1,1,1]],
]
weights_data=[
[[ 1, 0, 1],
[-1, 1, 0],
[ 0,-1, 0]],
[[-1, 0, 1],
[ 0, 0, 1],
[ 1, 1, 1]]
]
def get_shape(tensor):
[s1,s2,s3]= tensor.get_shape()
s1=int(s1)
s2=int(s2)
s3=int(s3)
return s1,s2,s3
def chw2hwc(chw_tensor):
[c,h,w]=get_shape(chw_tensor)
cols=[]
for i in range(c):
#每个通道里面的二维数组转为[w*h,1]即1列
line = tf.reshape(chw_tensor[i],[h*w,1])
cols.append(line)
#横向连接,即将所有竖直数组横向排列连接
input = tf.concat(cols,1)#[w*h,c]
#[w*h,c]-->[h,w,c]
input = tf.reshape(input,[h,w,c])
return input
def hwc2chw(hwc_tensor):
[h,w,c]=get_shape(hwc_tensor)
cs=[]
for i in range(c):
#[h,w]-->[1,h,w]
channel=tf.expand_dims(hwc_tensor[:,:,i],0)
cs.append(channel)
#[1,h,w]...[1,h,w]---->[c,h,w]
input = tf.concat(cs,0)#[c,h,w]
return input
def tf_conv2d(input,weights):
conv = tf.nn.conv2d(input, weights, strides=[1, 1, 1, 1], padding='SAME')
return conv
def main():
const_input = tf.constant(input_data , tf.float32)
const_weights = tf.constant(weights_data , tf.float32 )
input = tf.Variable(const_input,name="input")
#[2,5,5]------>[5,5,2]
input=chw2hwc(input)
#[5,5,2]------>[1,5,5,2]
input=tf.expand_dims(input,0)
weights = tf.Variable(const_weights,name="weights")
#[2,3,3]-->[3,3,2]
weights=chw2hwc(weights)
#[3,3,2]-->[3,3,2,1]
weights=tf.expand_dims(weights,3)
#[b,h,w,c]
conv=tf_conv2d(input,weights)
rs=hwc2chw(conv[0])
init=tf.global_variables_initializer()
sess=tf.Session()
sess.run(init)
conv_val = sess.run(rs)
print(conv_val[0])
if __name__=='__main__':
main()
卷积具体的执行过程:
上述卷积过程描述即为:
在计算卷积时,是卷积核与图像中每个mxm大小的图像块做element-wise相乘,然后得到的结果相加得到一个值,然后再移动一个stride,做同样的运算,直到整副输入图像遍历完,上述过程得到的值就组成了输出特征.
但是我们用的深度学习框架可不是这么实现的。那tensorflow中是如何实现卷积操作的呢.
采用im2col的过程.
上述为im2col卷积的详细过程.最终将卷积过程转为两个矩阵相乘.
输入feature map tensor: 1x3x3x3
卷积核tensor: 2x2x3x2
输出tensor: 1x2x2x2
给定 4-D input 和 filter tensors计算2-D卷积. 其中,
input tensor 的 shape是: [B, H, W, C],
kernel tensor 的 shape是: [filter_height, filter_width, in_channels, out_channels]
这个op是这样执行的:
将kernel filter 展开为一个 shape 为[filter_height * filter_width * in_channels, out_channels] 大小的2-D 矩阵。
从 input tensor按照每个filter位置上提取图像patches来构成一个虚拟的shape大小为[batch, out_height, out_width,filter_height * filter_width * in_channels]的tensor 。
对每个patch, 右乘以 filter matrix.得到[batch, out_height, out_width, out_channels]大小的输出。
参考: https://zhuanlan.zhihu.com/p/66958390
两个矩阵相乘即为通用矩阵乘(GEMM), 各种框架对gemm过程又做了各种优化.
矩阵A:MxK, 矩阵B: KxN, 相乘的gemm过程:
for (int m = 0; m < M; m++) {
for (int n = 0; n < N; n++) {
C[m][n] = 0;
for (int k = 0; k < K; k++) {
C[m][n] += A[m][k] * B[k][n];
}
}
}
该计算操作总数为:2^(MNK), (其中MNK分别指代三层循环执行的次数,2 指代循环最内层的一次乘法和加法), 内存访问操作总数为 4 (其中 4 指代对三者的内存访问, 需要先读取内存、累加完毕在存储,且忽略对初始化时的操作)。GEMM 的优化均以此为基点。
有很多的gemm优化算法.将最基础的计算改进了约七倍(如图二)。其基本方法是将输出划分为若干个 4×4子块,以提高对输入数据的重用。同时大量使用寄存器,减少访存;向量化访存和计算;消除指针计算;重新组织内存以地址连续等。详细的可以参考原文。
图三 将输出的计算拆分为 1×4 的小块,即将 维度拆分为两部分。计算该块输出时,需要使用 矩阵的 1 行,和 矩阵的 4 列.
for (int m = 0; m < M; m++) {
for (int n = 0; n < N; n += 4) {
C[m][n + 0] = 0;
C[m][n + 1] = 0;
C[m][n + 2] = 0;
C[m][n + 3] = 0;
for (int k = 0; k < K; k++) {
C[m][n + 0] += A[m][k] * B[k][n + 0];
C[m][n + 1] += A[m][k] * B[k][n + 1];
C[m][n + 2] += A[m][k] * B[k][n + 2];
C[m][n + 3] += A[m][k] * B[k][n + 3];
}
}
}
上述伪代码的最内侧计算使用的矩阵 的元素是一致的。因此可以将 [ ][ ] 读取到寄存器中,从而实现 4 次数据复用(这里不再给出示例)。一般将最内侧循环称作计算核(micro kernel)。进行这样的优化后,内存访问操作数量变为 (3+1/4) ,其中 1/4是优化的效果
类似, 可以继续优化.
详情请参考知乎:
https://zhuanlan.zhihu.com/p/66958390
2. 池化(pooling):用于减少特征数量
a. 最大值池化:可提取图片纹理
b. 均值池化:可保留背景特征