网络结构综述: 1.LeNet(1998)

在这里插入图片描述
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: 卷积神经网络开山之作.
主要创新点

  1. 局部感受野(local receptive fields):
  2. 权值共享(shared weights): 大大降低了参数的数量。
  3. 下采样(sub-sampling): 有效的降低输出对尺度和形变的敏感性。

重点概念

  1. 卷积
    参考: 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. 均值池化:可保留背景特征

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值