卷积神经网络学习心得——paddlepaddle浅析
近期跟随百度推出的关于深度学习课程,略微有些收获。在这里想要写点自己的心得与大家分享,这也是笔者第一次写博客肯定有很多书写错误和不到位的地方,还望各位大神多多指点。要是污了眼睛还望不要见怪。
1.卷积神经网络的概念:
卷积神经网络是目前计算机视觉中使用最普遍的模型结构。这里介绍卷积神经网络的一些基础模块,包括:
- 卷积(Convolution)
- 池化(Pooling)
- ReLU激活函数
- 批归一化(Batch Normalization)
- 丢弃法(Dropout)
在最开始的神经网络模型中,应用的是全连接层的特征提,取即将一张图片上的所有像素点展开成一个1维向量输入网络。但是这样的全连接层结构存在着两个重大的问题:
1.输入数据的空间信息被丢失;
2. 模型参数过多,容易发生过拟合。
为了解决以上问题,并使计算机视觉有更好的发展。卷积神经网络被提出,他总体的构架包括卷积层、池化层以及最后的全连接层作为输出。下图1是一个经典的卷积神经网络结构,多层卷积和池化层组合作用在输入图片上,在网络的最后通常会加入一系列全连接层,ReLU激活函数一般加在卷积或者全连接层的输出上,网络中通常还会加入Dropout来防止过拟合。
图一:卷积神经网络经典结构
卷积计算:卷积是数学分析中的一种积分变换的方法,在图像处理中采用的是卷积的离散形式。这里需要说明的是,在卷积神经网络中,卷积层的实现方式实际上是数学中定义的互相关 (cross-correlation)运算,与数学分析中的卷积定义有所不同,这里跟其他框架和卷积神经网络的教程保持一致,都使用互相关运算作为卷积的定义。
填充(padding):padding这个概念比较步长、滤波器或者卷积核来说在最后维度的计算上尤为的重要,所以记录下来。在paddlepaddle中padding可用常数或者list表示,如padding=2 表示在图像的长与宽都增加2,而padding=[2,3]就表示在图像的宽上增加2、在长上增加3。
感受野(Receptive Field):输出特征图上每个点的数值,是由输入图片上大小为kh×kwk_h\times k_wkh×kw的区域的元素与卷积核每个元素相乘再相加得到的,所以输入图像上kh×kwk_h\times k_wkh×kw区域内每个元素数值的改变,都会影响输出点的像素值。我们将这个区域叫做输出特征图上对应点的感受野。感受野内每个元素数值的变动,都会影响输出点的数值变化。比如3×33\times33×3卷积对应的感受野大小就是3×33\times33×3。
2.百度框架paddle关于卷积神经网络的API
- num_channels (int) - 输入图像的通道数。
- num_fliters (int) - 卷积核的个数,和输出特征图通道数相同,相当于上文中的CoutC_{out}Cout。
- filter_size(int|tuple) - 卷积核大小,可以是整数,比如3,表示卷积核的高和宽均为3 ;或者是两个整数的list,例如[3,2],表示卷积核的高为3,宽为2。
- stride(int|tuple) - 步幅,可以是整数,默认值为1,表示垂直和水平滑动步幅均为1;或者是两个整数的list,例如[3,2],表示垂直滑动步幅为3,水平滑动步幅为2。
- padding(int|tuple) - 填充大小,可以是整数,比如1,表示竖直和水平边界填充大小均为1;或者是两个整数的list,例如[2,1],表示竖直边界填充大小为2,水平边界填充大小为1。
- act(str)- 应用于输出上的激活函数,如Tanh、Softmax、Sigmoid,Relu等,默认值为None。
输入数据维度[N,Cin,Hin,Win][N, C_{in}, H_{in}, W_{in}][N,Cin,Hin,Win],输出数据维度[N,num_filters,Hout,Wout][N, num\_filters, H_{out}, W_{out}][N,num_filters,Hout,Wout],权重参数www的维度[num_filters,Cin,filter_size_h,filter_size_w][num\_filters, C_{in}, filter\_size\_h, filter\_size\_w][num_filters,Cin,filter_size_h,filter_size_w],偏置参数bbb的维度是[num_filters][num\_filters][num_filters]。
以上输入充分体现了我这个小白啥也不是,具体请前往百度飞浆官网查看:https://www.paddlepaddle.org.cn/
3.五种卷积神经网络模型
(1)LeNet
class LeNet(fluid.dygraph.Layer):
def __init__(self, num_classes=1):
super(LeNet, self).__init__()
# 创建卷积和池化层块,每个卷积层使用Sigmoid激活函数,后面跟着一个2x2的池化
self.conv1 = Conv2D(num_channels=3, num_filters=6, filter_size=5, act='sigmoid')
self.pool1 = Pool2D(pool_size=2, pool_stride=2, pool_type='max')
self.conv2 = Conv2D(num_channels=6, num_filters=16, filter_size=5, act='sigmoid')
self.pool2 = Pool2D(pool_size=2, pool_stride=2, pool_type='max')
# 创建第3个卷积层
self.conv3 = Conv2D(num_channels=16, num_filters=120, filter_size=4, act='sigmoid')
# 创建全连接层,第一个全连接层的输出神经元个数为64, 第二个全连接层输出神经元个数为分类标签的类别数
self.fc1 = Linear(input_dim=300000, output_dim=64, act='sigmoid')
self.fc2 = Linear(input_dim=64, output_dim=num_classes)
# 网络的前向计算过程
def forward(self, x, label=None):
x = self.conv1(x)
x = self.pool1(x)
x = self.conv2(x)
x = self.pool2(x)
x = self.conv3(x)
x = fluid.layers.reshape(x, [x.shape[0], -1])
x = self.fc1(x)
x = self.fc2(x)
if label is not None:
acc = fluid.layers.accuracy(input=x, label=label)
return x, acc
else:
return x
(2)AlexNet
# -*- coding:utf-8 -*-
# 导入需要的包
import paddle
import paddle.fluid as fluid
import numpy as np
from paddle.fluid.dygraph.nn import Conv2D, Pool2D, Linear
# 定义 AlexNet 网络结构
class AlexNet(fluid.dygraph.Layer):
def __init__(self, num_classes=1):
super(AlexNet, self).__init__()
# AlexNet与LeNet一样也会同时使用卷积和池化层提取图像特征
# 与LeNet不同的是激活函数换成了‘relu’
self.conv1 = Conv2D(num_channels=3, num_filters=96, filter_size=11, stride=4, padding=5, act='relu')
self.pool1 = Pool2D(pool_size=2, pool_stride=2, pool_type='max')
self.conv2 = Conv2D(num_channels=96, num_filters=256, filter_size=5, stride=1, padding=2, act='relu')
self.pool2 = Pool2D(pool_size=2, pool_stride=2, pool_type='max')
self.conv3 = Conv2D(num_channels=256, num_filters=384, filter_size=3, stride=1, padding=1, act='relu')
self.conv4 = Conv2D(num_channels=384, num_filters=384, filter_size=3, stride=1, padding=1, act='relu')
self.conv5 = Conv2D(num_channels=384, num_filters=256, filter_size=3, stride=1, padding=1, act='relu')
self.pool5 = Pool2D(pool_size=2, pool_stride=2, pool_type='max')
self.fc1 = Linear(input_dim=12544, output_dim=4096, act='relu')
self.drop_ratio1 = 0.5
self.fc2 = Linear(input_dim=4096, output_dim=4096, act='relu')
self.drop_ratio2 = 0.5
self.fc3 = Linear(input_dim=4096, output_dim=num_classes)
def forward(self, x):
x = self.conv1(x)
x = self.pool1(x)
x = self.conv2(x)
x = self.pool2(x)
x = self.conv3(x)
x = self.conv4(x)
x = self.conv5(x)
x = self.pool5(x)
x = fluid.layers.reshape(x, [x.shape[0], -1])
x = self.fc1(x)
# 在全连接之后使用dropout抑制过拟合
x= fluid.layers.dropout(x, self.drop_ratio1)
x = self.fc2(x)
# 在全连接之后使用dropout抑制过拟合
x = fluid.layers.dropout(x, self.drop_ratio2)
x = self.fc3(x)
return x
(3)VGG
# -*- coding:utf-8 -*-
# VGG模型代码
import numpy as np
import paddle
import paddle.fluid as fluid
from paddle.fluid.layer_helper import LayerHelper
from paddle.fluid.dygraph.nn import Conv2D, Pool2D, BatchNorm, Linear
from paddle.fluid.dygraph.base import to_variable
# 定义vgg块,包含多层卷积和1层2x2的最大池化层
class vgg_block(fluid.dygraph.Layer):
def __init__(self, num_convs, in_channels, out_channels):
"""
num_convs, 卷积层的数目
num_channels, 卷积层的输出通道数,在同一个Incepition块内,卷积层输出通道数是一样的
"""
super(vgg_block, self).__init__()
self.conv_list = []
for i in range(num_convs):
conv_layer = self.add_sublayer('conv_' + str(i), Conv2D(num_channels=in_channels,
num_filters=out_channels, filter_size=3, padding=1, act='relu'))
self.conv_list.append(conv_layer)
in_channels = out_channels
self.pool = Pool2D(pool_stride=2, pool_size = 2, pool_type='max')
def forward(self, x):
for item in self.conv_list:
x = item(x)
return self.pool(x)
class VGG(fluid.dygraph.Layer):
def __init__(self, conv_arch=((2, 64),
(2, 128), (3, 256), (3, 512), (3, 512))):
super(VGG, self).__init__()
self.vgg_blocks=[]
iter_id = 0
# 添加vgg_block
# 这里一共5个vgg_block,每个block里面的卷积层数目和输出通道数由conv_arch指定
in_channels = [3, 64, 128, 256, 512, 512]
for (num_convs, num_channels) in conv_arch:
block = self.add_sublayer('block_' + str(iter_id),
vgg_block(num_convs, in_channels=in_channels[iter_id],
out_channels=num_channels))
self.vgg_blocks.append(block)
iter_id += 1
self.fc1 = Linear(input_dim=512*7*7, output_dim=4096,
act='relu')
self.drop1_ratio = 0.5
self.fc2= Linear(input_dim=4096, output_dim=4096,
act='relu')
self.drop2_ratio = 0.5
self.fc3 = Linear(input_dim=4096, output_dim=1)
def forward(self, x):
for item in self.vgg_blocks:
x = item(x)
x = fluid.layers.reshape(x, [x.shape[0], -1])
x = fluid.layers.dropout(self.fc1(x), self.drop1_ratio)
x = fluid.layers.dropout(self.fc2(x), self.drop2_ratio)
x = self.fc3(x)
return x
(4)GoogLeNet
class Inception(fluid.dygraph.Layer):
def __init__(self, c1, c2, c3, c4, **kwargs):
'''
Inception模块的实现代码,
c1, 图(b)中第一条支路1x1卷积的输出通道数,数据类型是整数
c2,图(b)中第二条支路卷积的输出通道数,数据类型是tuple或list,
其中c2[0]是1x1卷积的输出通道数,c2[1]是3x3
c3,图(b)中第三条支路卷积的输出通道数,数据类型是tuple或list,
其中c3[0]是1x1卷积的输出通道数,c3[1]是3x3
c4, 图(b)中第一条支路1x1卷积的输出通道数,数据类型是整数
'''
super(Inception, self).__init__()
# 依次创建Inception块每条支路上使用到的操作
self.p1_1 = Conv2D(num_filters=c1,
filter_size=1, act='relu')
self.p2_1 = Conv2D(num_filters=c2[0],
filter_size=1, act='relu')
self.p2_2 = Conv2D(num_filters=c2[1],
filter_size=3, padding=1, act='relu')
self.p3_1 = Conv2D(num_filters=c3[0],
filter_size=1, act='relu')
self.p3_2 = Conv2D(num_filters=c3[1],
filter_size=5, padding=2, act='relu')
self.p4_1 = Pool2D(pool_size=3,
pool_stride=1, pool_padding=1,
pool_type='max')
self.p4_2 = Conv2D(num_filters=c4,
filter_size=1, act='relu')
def forward(self, x):
# 支路1只包含一个1x1卷积
p1 = self.p1_1(x)
# 支路2包含 1x1卷积 + 3x3卷积
p2 = self.p2_2(self.p2_1(x))
# 支路3包含 1x1卷积 + 5x5卷积
p3 = self.p3_2(self.p3_1(x))
# 支路4包含 最大池化和1x1卷积
p4 = self.p4_2(self.p4_1(x))
# 将每个支路的输出特征图拼接在一起作为最终的输出结果
return fluid.layers.concat([p1, p2, p3, p4], axis=1)
(5)ResNet
# ResNet中使用了BatchNorm层,在卷积层的后面加上BatchNorm以提升数值稳定性
# 定义卷积批归一化块
class ConvBNLayer(fluid.dygraph.Layer):
def __init__(self,
num_channels,
num_filters,
filter_size,
stride=1,
groups=1,
act=None):
"""
num_channels, 卷积层的输入通道数
num_filters, 卷积层的输出通道数
stride, 卷积层的步幅
groups, 分组卷积的组数,默认groups=1不使用分组卷积
act, 激活函数类型,默认act=None不使用激活函数
"""
super(ConvBNLayer, self).__init__()
# 创建卷积层
self._conv = Conv2D(
num_channels=num_channels,
num_filters=num_filters,
filter_size=filter_size,
stride=stride,
padding=(filter_size - 1) // 2,
groups=groups,
act=None,
bias_attr=False)
# 创建BatchNorm层
self._batch_norm = BatchNorm(num_filters, act=act)
def forward(self, inputs):
y = self._conv(inputs)
y = self._batch_norm(y)
return y
# 定义残差块
# 每个残差块会对输入图片做三次卷积,然后跟输入图片进行短接
# 如果残差块中第三次卷积输出特征图的形状与输入不一致,则对输入图片做1x1卷积,将其输出形状调整成一致
class BottleneckBlock(fluid.dygraph.Layer):
def __init__(self,
num_channels,
num_filters,
stride,
shortcut=True):
super(BottleneckBlock, self).__init__()
# 创建第一个卷积层 1x1
self.conv0 = ConvBNLayer(
num_channels=num_channels,
num_filters=num_filters,
filter_size=1,
act='relu')
# 创建第二个卷积层 3x3
self.conv1 = ConvBNLayer(
num_channels=num_filters,
num_filters=num_filters,
filter_size=3,
stride=stride,
act='relu')
# 创建第三个卷积 1x1,但输出通道数乘以4
self.conv2 = ConvBNLayer(
num_channels=num_filters,
num_filters=num_filters * 4,
filter_size=1,
act=None)
# 如果conv2的输出跟此残差块的输入数据形状一致,则shortcut=True
# 否则shortcut = False,添加1个1x1的卷积作用在输入数据上,使其形状变成跟conv2一致
if not shortcut:
self.short = ConvBNLayer(
num_channels=num_channels,
num_filters=num_filters * 4,
filter_size=1,
stride=stride)
self.shortcut = shortcut
self._num_channels_out = num_filters * 4
def forward(self, inputs):
y = self.conv0(inputs)
conv1 = self.conv1(y)
conv2 = self.conv2(conv1)
# 如果shortcut=True,直接将inputs跟conv2的输出相加
# 否则需要对inputs进行一次卷积,将形状调整成跟conv2输出一致
if self.shortcut:
short = inputs
else:
short = self.short(inputs)
y = fluid.layers.elementwise_add(x=short, y=conv2)
layer_helper = LayerHelper(self.full_name(), act='relu')
return layer_helper.append_activation(y)
# 定义ResNet模型
class ResNet(fluid.dygraph.Layer):
def __init__(self, layers=50, class_dim=1):
"""
layers, 网络层数,可以是50, 101或者152
class_dim,分类标签的类别数
"""
super(ResNet, self).__init__()
self.layers = layers
supported_layers = [50, 101, 152]
assert layers in supported_layers, \
"supported layers are {} but input layer is {}".format(supported_layers, layers)
if layers == 50:
#ResNet50包含多个模块,其中第2到第5个模块分别包含3、4、6、3个残差块
depth = [3, 4, 6, 3]
elif layers == 101:
#ResNet101包含多个模块,其中第2到第5个模块分别包含3、4、23、3个残差块
depth = [3, 4, 23, 3]
elif layers == 152:
#ResNet50包含多个模块,其中第2到第5个模块分别包含3、8、36、3个残差块
depth = [3, 8, 36, 3]
# 残差块中使用到的卷积的输出通道数
num_filters = [64, 128, 256, 512]
# ResNet的第一个模块,包含1个7x7卷积,后面跟着1个最大池化层
self.conv = ConvBNLayer(
num_channels=3,
num_filters=64,
filter_size=7,
stride=2,
act='relu')
self.pool2d_max = Pool2D(
pool_size=3,
pool_stride=2,
pool_padding=1,
pool_type='max')
# ResNet的第二到第五个模块c2、c3、c4、c5
self.bottleneck_block_list = []
num_channels = 64
for block in range(len(depth)):
shortcut = False
for i in range(depth[block]):
bottleneck_block = self.add_sublayer(
'bb_%d_%d' % (block, i),
BottleneckBlock(
num_channels=num_channels,
num_filters=num_filters[block],
stride=2 if i == 0 and block != 0 else 1, # c3、c4、c5将会在第一个残差块使用stride=2;其余所有残差块stride=1
shortcut=shortcut))
num_channels = bottleneck_block._num_channels_out
self.bottleneck_block_list.append(bottleneck_block)
shortcut = True
# 在c5的输出特征图上使用全局池化
self.pool2d_avg = Pool2D(pool_size=7, pool_type='avg', global_pooling=True)
# stdv用来作为全连接层随机初始化参数的方差
import math
stdv = 1.0 / math.sqrt(2048 * 1.0)
# 创建全连接层,输出大小为类别数目
self.out = Linear(input_dim=2048, output_dim=class_dim,
param_attr=fluid.param_attr.ParamAttr(
initializer=fluid.initializer.Uniform(-stdv, stdv)))
def forward(self, inputs):
y = self.conv(inputs)
y = self.pool2d_max(y)
for bottleneck_block in self.bottleneck_block_list:
y = bottleneck_block(y)
y = self.pool2d_avg(y)
y = fluid.layers.reshape(y, [y.shape[0], -1])
y = self.out(y)
return y
上述五个网络模型都是采用的百度飞浆深度学习框架paddle写成,在notebook上可直接运行的代码
4.我的心得
在自己的不断学习当中认识自己有太多的不足,不过也有相当可观的收获。在代码实践方面自己有太多的不足,所以只能成段成段的取用编写好的代码来进行修改,而一旦代码复杂起来,自己所积累的知识库就瞬间瓦解。所以在基础知识和代码实践上自己还有相当长的一段路要走。
其实在初次听到卷积神经网络这个词的时候,我觉得这是非常高大上的专业词汇。随着不断的学习自己也从仅仅知道这个名字的小白,变成了了解它总体构架与大部分理论基础的小白。虽然都是小白,但差距也可以用天差地别来形容。嗯~就这样,继续去努力学习吧