计算机视觉
- 计算机视觉概述
- 视觉的起源和发展
- 从生物视觉到机器视觉
- 计算机视觉的主要任务
- 计算机视觉的发展历程
- 图像识别的基本框架
- 早期图像处理方法
- 中期图像处理方法
- 深度学应用于图像处理
- 计算机视觉的应用场景
- 卷积神经网络基础
- 卷积神经网络的主要内容
- 为什么引入卷积神经网络
- 卷积神经网络的主要模块
- 卷积(Convolution)
- 卷积计算
- 卷积的特点
- 填充(padding)
- 步幅(stride)
- 感受野(Receptive Field)
- 多输入通道、多输出通道和批量操作
- 卷积应用举例
- 飞桨卷积API介绍
- 案例1:边缘检测
- 案例2:边缘检测
- 案例3:均值模糊
- 池化(Pooling)
- 激活函数:Relu和Sigmoid
- 批归一化(Batch Normalization)
- 丢弃法(Dropout)
- 图像分类
- LeNet
- AlexNet
- VGG
- GoogLeNet
- ResNet
计算机视觉作为一门让机器学会如何去“看”的科学学科,具体的说,就是让机器去
识别
摄像机拍摄的图片或视频中的物体,
检测
出物体所在的位置,并对目标物体进行
跟踪
,从而
理解并描述
出图片或视频里的场景和故事,以此来
模拟
人脑视觉系统。
因此,计算机视觉也通常被叫做
机器视觉
,其目的是建立能够从图像或者视频中“感知”信息的人工系统。
计算机视觉技术经过几十年的发展,已经在交通(车牌识别、道路违章抓拍)、安防(人脸闸机、小区监控)、金融(刷脸支付、柜台的自动票据识别)、医疗(医疗影像诊断)、工业生产(产品缺陷自动检测)等多个领域应用,影响或正在改变人们的日常生活和工业生产方式。未来,随着技术的不断演进,必将涌现出更多的产品应用,为我们的生活创造更大的便利和更广阔的机会。
本篇文章将重点介绍计算机视觉的经典模型(卷积神经网络)和两个典型任务(图像分类和目标检测)。主要涵盖如下内容:
- 卷积神经网络:卷积神经网络(Convolutional Neural Networks, CNN)是计算机视觉技术最经典的模型结构。本教程主要介绍卷积神经网络的常用模块,包括:卷积、池化、激活函数、批归一化、Dropout等。
- 图像分类:介绍图像分类算法的经典模型结构,包括:LeNet、AlexNet、VGG、GoogLeNet、ResNet,并通过眼疾筛查的案例展示算法的应用。
- 目标检测:介绍目标检测YOLO-V3算法,并通过林业病虫害检测案例展示YOLO-V3算法的应用。
计算机视觉概述
视觉的起源和发展
从生物视觉到机器视觉
计算机视觉的主要任务
- (a) Image Classification: 图像分类,用于
识别
图像中物体的类别
(如:bottle、cup、cube)。 - (b) Object Localization: 目标检测,用于
检测
图像中每个物体的类别
,并准确标出它们的位置。 - (c ) Semantic Segmentation: 图像语义分割,用于
标出
图像中每个像素点所属的类别
,属于同一类别的像素点用一个颜色标识。 - (d) Instance Segmentation: 实例分割,值得注意的是,(b)中的目标检测任务只需要标注出物体位置,而(d)中的实例分割任务
不仅要标注出物体位置,还需要标注出物体的外形轮廓
。
计算机视觉的发展历程
图像识别的基本框架
早期图像处理方法
中期图像处理方法
深度学应用于图像处理
计算机视觉的应用场景
卷积神经网络基础
卷积神经网络的主要内容
为什么引入卷积神经网络
为了解决上述问题,我们引入卷积神经网络进行特征提取,既能提取到相邻像素点之间的特征模式,又能保证参数的个数不随图片尺寸变化下图是一个典型的卷积神经网络结构,多层卷积和池化层组合作用在输入图片上,在网络的最后通常会加入一系列全连接层,ReLU激活函数一般加在卷积或者全连接层的输出上,网络中通常还会加入Dropout来防止过拟合。
卷积神经网络的主要模块
卷积(Convolution)
卷积计算
卷积的特点
填充(padding)
步幅(stride)
当宽和高方向的步幅分别为和,填充为和,卷积核大小为时,输出特征图尺寸的计算公式是:
感受野(Receptive Field)
输出特征图上每个点的数值,是由输入图片上大小为的区域的元素与卷积核每个元素相乘再相加得到的,所以输入图像上区域内每个元素数值的改变,都会影响输出点的像素值。我们将这个区域叫做输出特征图上对应点的感受野
。
感受野内每个元素数值的变动,都会影响输出点的数值变化。比如3×33\times33×3卷积对应的感受野大小就是
而当通过两层3×33\times33×3的卷积之后,感受野的大小将会增加到
多输入通道、多输出通道和批量操作
问题:为什么把三个通道的数据相加之后再输出?有什么数学依据吗?
一般来说,卷积操作的输出特征图也会具有多个通道 ,这时我们需要设计 个维度为的卷积核,卷积核数组的维度是问题:多输出通道有什么用呢?
在卷积神经网络的计算中,通常将多个样本放在一起形成一个mini-batch进行批量操作,即输入数据的维度是。由于会对每张图片使用同样的卷积核进行卷积操作,卷积核的维度与上面多输出通道的情况一样,仍然是,输出特征图的维度是
卷积应用举例
飞桨卷积API介绍
飞桨卷积算子对应的API是paddle.fluid.dygraph.Conv2D
- num_channels (int) - 输入图像的通道数。
- num_fliters (int) - 卷积核的个数,和输出特征图通道数相同,相当于上文中的
- 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。
输入数据维度,输出数据维度,权重参数的维度,偏置参数的维度是。
案例1:边缘检测
下面是使用Conv2D算子完成一个图像边界检测的任务。图像左边为光亮部分,右边为黑暗部分,需要检测出光亮跟黑暗的分界处。
import matplotlib.pyplot as plt
import numpy as np
import paddle
import paddle.fluid as fluid
from paddle.fluid.dygraph.nn import Conv2D
from paddle.fluid.initializer import NumpyArrayInitializer
with fluid.dygraph.guard():
# 创建初始化权重参数w
w = np.array([-1, 0, 1], dtype='float32')
# 将权重参数调整成维度为[cout, cin, kn, kw]的四维张量
w = w.reshape([1, 1, 1, 3])
# 创建卷积算子,设置输出通道数,卷积核大小,和初始化权重参数
# filter_size = [1, 3]表示kh = 1, kw=3
# 创建卷积算子的时候,通过参数属性param_attr,指定参数初始化方式
# 这里的初始化方式时,从numpy.ndarray初始化卷积参数
conv = Conv2D(num_channels=1, # 输出通道数
num_filters=1, # 输入通道数
filter_size=[1, 3],
param_attr=fluid.ParamAttr(
initializer=NumpyArrayInitializer(value=w)
)
)
# 创建输入图片,图片左边像素取值为1,右边像素取值为0
img = np.ones([50, 50], dtype='float32')
img[:, :30] = 0
# 将图片形状调整为[N, C, H, W]的形式
x = img.reshape([1, 1, 50, 50])
# 将nimpy.array转化为paddle中的tensor
x = fluid.dygraph.to_variable(x)
# 将卷积算子作用在输入图片上
y = conv(x)
# 将输出tensor转化为numpy.array
out = y.numpy()
f = plt.subplot(121)
f.set_title('input image', fontsize=15)
plt.imshow(img, cmap='gray')
f = plt.subplot(122)
f.set_title('output featuremap', fontsize=15)
# 卷积算子Conv2D输出数据形状为[N, C, H, W]形式
# 此处N, C=1,输出数据形状为[1, 1, H, W],是4维数组
# 但是画图函数plt.imshow画灰度图时,只接受2维数组
# 通过numpy.squeeze函数将大小为1的维度消除
plt.imshow(out.squeeze(), cmap='gray')
plt.show()
# 查看卷积层的参数
with fluid.dygraph.guard():
# 通过 conv.parameters()查看卷积层的参数,返回值是list,包含两个元素
print(conv.parameters())
# 查看卷积层的权重参数名字和数值
print(conv.parameters()[0].name, conv.parameters()[0].numpy())
# 参看卷积层的偏置参数名字和数值
print(conv.parameters()[1].name, conv.parameters()[1].numpy())
案例2:边缘检测
上面展示的是一个人为构造出来的简单图片使用卷积检测明暗分界处的例子,对于真实的图片,也可以使用合适的卷积核对它进行操作,用来检测物体的外形轮廓,观察输出特征图跟原图之间的对应关系,如下代码所示:
import matplotlib.pyplot as plt
from PIL import Image
import numpy as np
import paddle
import paddle.fluid as fluid
from paddle.fluid.dygraph.nn import Conv2D
from paddle.fluid.initializer import NumpyArrayInitializer
img = Image.open('/home/al007/Computer vision/fly.jpg')
with fluid.dygraph.guard():
# 设置卷积核参数
w = np.array([[-1,-1,-1], [-1,8,-1], [-1,-1,-1]], dtype='float32')/8
w = w.reshape([1, 1, 3, 3])
# 由于输入通道数是3,将卷积核的形状从[1,1,3,3]调整为[1,3,3,3]
w = np.repeat(w, 3, axis=1)
# 创建卷积算子,输出通道数为1,卷积核大小为3x3,
# 并使用上面的设置好的数值作为卷积核权重的初始化参数
conv = Conv2D(num_channels=3,
num_filters=1,
filter_size=[3, 3],
param_attr=fluid.ParamAttr(
initializer=NumpyArrayInitializer(value=w)
)
)
# 将读入的图片转化为float32类型的numpy.array
x = np.array(img).astype('float32')
# 图片读入成ndarry时,形状是[H, W, 3]
# 将通道这一维度调整到最前面
x = np.transpose(x, (2, 0, 1))
# 将数据形状调整为[N, Cin, H, W]格式
x = x.reshape(1, 3, img.height, img.width)
# 将nimpy.array转化为paddle中的tensor
x = fluid.dygraph.to_variable(x)
# 将卷积算子作用在输入图片上
y = conv(x)
# 将输出tensor转化为numpy.array
out = y.numpy()
plt.figure(figsize=(20, 10))
f = plt.subplot(121)
f.set_title('input image', fontsize=15)
plt.imshow(img)
f = plt.subplot(122)
f.set_title('output feature map', fontsize=15)
plt.imshow(out.squeeze(), cmap='gray')
plt.show()
案例3:均值模糊
另外一种比较常见的卷积核是用当前像素跟它邻域内的像素取平均,这样可以使图像上噪声比较大的点变得更平滑,如下代码所示:
import matplotlib.pyplot as plt
from PIL import Image
import numpy as np
import paddle
import paddle.fluid as fluid
from paddle.fluid.dygraph.nn import Conv2D
from paddle.fluid.initializer import NumpyArrayInitializer
# 读入图片并转成numpy.ndarray
img = Image.open('/home/al007/Computer vision/bear.jpg').convert('L') # PIL有九种不同模式: 1,L,P,RGB,RGBA,CMYK,YCbCr,I,F。
with fluid.dygraph.guard():
# 创建初始化参数
w = np.ones([1, 1, 5, 5], dtype = 'float32')/25
conv = Conv2D(num_channels=1,
num_filters=1,
filter_size=[5, 5],
param_attr=fluid.ParamAttr(
initializer=NumpyArrayInitializer(value=w)
)
)
x = np.array(img).astype('float32')
x = x.reshape(1, 1, x.shape[0], x.shape[1])
x = fluid.dygraph.to_variable(x)
y = conv(x)
out = y.numpy()
plt.figure(figsize=(20, 12))
f = plt.subplot(121)
f.set_title('input image')
plt.imshow(img, cmap='gray')
f = plt.subplot(122)
f.set_title('output feature map')
out = out.squeeze()
plt.imshow(out, cmap='gray')
plt.show()
池化(Pooling)
池化是使用某一位置的相邻输出的总体统计特征代替网络在该位置的输出,其好处是当输入数据做出少量平移时,经过池化函数后的大多数输出还能保持不变。
比如:当识别一张图像是否是人脸时,我们需要知道人脸左边有一只眼睛,右边也有一只眼睛,而不需要知道眼睛的精确位置,这时候通过池化某一片区域的像素点来得到总体统计特征会显得很有用。
由于池化之后特征图会变得更小,如果后面连接的是全连接层,能有效的减小神经元的个数,节省存储空间并提高计算效率。
通常有两种方法,平均池化和最大池化。
激活函数:Relu和Sigmoid
# ReLU和Sigmoid激活函数示意图
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.patches as patches
plt.figure(figsize=(10, 5))
# 创建数据x
x = np.arange(-10, 10, 0.1)
# 计算Sigmoid函数
s = 1.0 / (1 + np.exp(0. - x))
# 计算ReLU函数
y = np.clip(x, a_min=0., a_max=None)
#####################################
# 以下部分为画图代码
f = plt.subplot(121)
plt.plot(x, s, color='r')
currentAxis=plt.gca()
plt.text(-9.0, 0.9, r'$y=Sigmoid(x)$', fontsize=13)
currentAxis.xaxis.set_label_text('x', fontsize=15)
currentAxis.yaxis.set_label_text('y', fontsize=15)
f = plt.subplot(122)
plt.plot(x, y, color='g')
plt.text(-3.0, 9, r'$y=ReLU(x)$', fontsize=13)
currentAxis=plt.gca()
currentAxis.xaxis.set_label_text('x', fontsize=15)
currentAxis.yaxis.set_label_text('y', fontsize=15)
批归一化(Batch Normalization)
其目的是对神经网络中间层的输出进行标准化处理,使得中间层的输出更加稳定。
# 输入数据形状是 [N, K]时的示例
import numpy as np
import paddle
import paddle.fluid as fluid
from paddle.fluid.dygraph.nn import BatchNorm
# 创建数据
data = np.array([[1,2,3], [4,5,6], [7,8,9]]).astype('float32')
# 使用BatchNorm计算归一化的输出
with fluid.dygraph.guard():
# 输入数据维度[N, K],num_channels等于K
bn = BatchNorm(num_channels=3)
x = fluid.dygraph.to_variable(data)
y = bn(x)
print('output of BatchNorm Layer: \n {}'.format(y.numpy()))
# 使用Numpy计算均值、方差和归一化的输出
# 这里对第0个特征进行验证
a = np.array([1,4,7])
a_mean = a.mean()
a_std = a.std()
b = (a - a_mean) / a_std
print('std {}, mean {}, \n output {}'.format(a_mean, a_std, b))
# 建议读者对第1和第2个特征进行验证,观察numpy计算结果与paddle计算结果是否一致
# 输入数据形状是[N, C, H, W]时的batchnorm示例
import numpy as np
import paddle
import paddle.fluid as fluid
from paddle.fluid.dygraph.nn import BatchNorm
# 设置随机数种子,这样可以保证每次运行结果一致
np.random.seed(100)
# 创建数据
data = np.random.rand(2,3,3,3).astype('float32')
# 使用BatchNorm计算归一化的输出
with fluid.dygraph.guard():
# 输入数据维度[N, C, H, W],num_channels等于C
bn = BatchNorm(num_channels=3)
x = fluid.dygraph.to_variable(data)
y = bn(x)
print('input of BatchNorm Layer: \n {}'.format(x.numpy()))
print('output of BatchNorm Layer: \n {}'.format(y.numpy()))
# 取出data中第0通道的数据,
# 使用numpy计算均值、方差及归一化的输出
a = data[:, 0, :, :]
a_mean = a.mean()
a_std = a.std()
b = (a - a_mean) / a_std
print('channel 0 of input data: \n {}'.format(a))
print('std {}, mean {}, \n output: \n {}'.format(a_mean, a_std, b))
# 提示:这里通过numpy计算出来的输出
# 与BatchNorm算子的结果略有差别,
# 因为在BatchNorm算子为了保证数值的稳定性,
# 在分母里面加上了一个比较小的浮点数epsilon=1e-05
样本A的预测结果就会变得不确定,这对预测过程来说是不合理的。解决方法是在训练过程中将大量样本的均值和方差保存下来,预测时直接使用保存好的值而不再重新计算。
在训练过程的最开始将和设置为0,每次输入一批新的样本,计算出和,然后通过上面的公式更新和,在训练的过程中不断的更新它们的值,并作为BatchNorm层的参数保存下来。预测的时候将会加载参数和 ,用他们来代替和。
丢弃法(Dropout)
import numpy as np
import paddle
import paddle.fluid as fluid
# 设置随数机种子,这样可以保证每次运行结果一致
np.random.seed(100)
# 创建数据[N, C, H, W], 一般对应卷基层的输出
data1 = np.random.rand(2, 3, 3, 3).astype('float32')
# 创建数据[N, K],一般对应全连接层的输出
data2 = np.arange(1, 13).reshape([-1, 3]).astype('float32')
# print(data2)
# 使用dropout作用在输入数据上
with fluid.dygraph.guard():
# 将nimpy.array转化为paddle中的tensor
x1 = fluid.dygraph.to_variable(data1)
# downgrade_in_infer模式下
drop11 = fluid.dygraph.Dropout(p=0.5, # 输入单元的丢弃概率
dropout_implementation='downgrade_in_infer') # 丢弃法的实现方式
droped_train11 = drop11(x1)
# 切换到eval模式
drop11.eval() # 标记是否是测试阶段,对于动态图模式,请使用 eval() 接口。
droped_eval11 = drop11(x1)
# upscale_in_train模式下
drop12 = fluid.dygraph.Dropout(p=0.5,
dropout_implementation='upscale_in_train')
droped_train12 = drop12(x1)
# 切换到eval模式
drop12.eval()
droped_eval12 = drop12(x1)
x2 = fluid.dygraph.to_variable(data2)
drop21 = fluid.dygraph.Dropout(p = 0.5,
dropout_implementation = 'downgrade_in_infer')
droped_train21 = drop21(x2)
# 切换到eval模式
drop21.eval()
droped_eval21 = drop21(x2)
# upscale_in_train模式下
drop22 = fluid.dygraph.Dropout(p = 0.5,
dropout_implementation = 'upscale_in_train')
droped_train22 = drop22(x2)
# 切换到eval模式
drop22.eval()
droped_eval22 = drop22(x2)
print('x1 {}, \n droped_train11 \n {}, \n droped_eval11 \n {}'.format(data1, droped_train11.numpy(), droped_eval11.numpy()))
print('x1 {}, \n droped_train12 \n {}, \n droped_eval12 \n {}'.format(data1, droped_train12.numpy(), droped_eval12.numpy()))
print('x2 {}, \n droped_train21 \n {}, \n droped_eval21 \n {}'.format(data2, droped_train21.numpy(), droped_eval21.numpy()))
print('x2 {}, \n droped_train22 \n {}, \n droped_eval22 \n {}'.format(data2, droped_train22.numpy(), droped_eval22.numpy()))
图像分类
图像分类是根据图像的语义信息对不同类别图像进行区分,是计算机视觉的核心,是物体检测、图像分割、物体跟踪、行为分析、人脸识别等其他高层次视觉任务的基础。
图像分类在许多领域都有着广泛的应用,如:安防领域的人脸识别和智能视频分析等,交通领域的交通场景识别,互联网领域基于内容的图像检索和相册自动归类,医学领域的图像识别等。
上一节主要介绍了卷积神经网络常用的一些基本模块,本节将基于眼疾分类数据集iChallenge-PM,对图像分类领域的经典卷积神经网络进行剖析,介绍如何应用这些基础模块构建卷积神经网络,解决图像分类问题。
涵盖如下卷积神经网络:
- LeNet:Yan LeCun等人于1998年第一次将卷积神经网络应用到图像分类任务上[1],在手写数字识别任务上取得了巨大成功。
- AlexNet:Alex Krizhevsky等人在2012年提出了AlexNet[2], 并应用在大尺寸图片数据集ImageNet上,获得了2012年ImageNet比赛冠军(ImageNet Large Scale Visual Recognition Challenge,ILSVRC)。
- VGG:Simonyan和Zisserman于2014年提出了VGG网络结构[3],是当前最流行的卷积神经网络之一,由于其结构简单、应用性极强而深受广大研究者欢迎。
- GoogLeNet:Christian Szegedy等人在2014提出了GoogLeNet[4],并取得了2014年ImageNet比赛冠军。
- ResNet:Kaiming He等人在2015年提出了ResNet[5],通过引入残差模块加深网络层数,在ImagNet数据集上的错误率降低到3.6%,超越了人眼识别水平。ResNet的设计思想深刻地影响了后来的深度神经网络的设计。
LeNet
LeNet是最早的卷积神经网络之一[1]。1998年,Yan LeCun第一次将LeNet卷积神经网络应用到图像分类上,在手写数字识别任务中取得了巨大成功。LeNet通过连续使用卷积和池化层的组合提取图像特征,其架构如图所示,这里展示的是作者论文中的LeNet-5模型:
- 第一模块:包含5×5的6通道卷积和2×2的池化。卷积提取图像中包含的特征模式(激活函数使用sigmoid),图像尺寸从32减小到28。经过池化层可以降低输出特征图对空间位置的敏感性,图像尺寸减到14。
- 第二模块:和第一模块尺寸相同,通道数由6增加为16。卷积操作使图像尺寸减小到10,经过池化后变成5。
- 第三模块:包含5×5的120通道卷积。卷积之后的图像尺寸减小到1,但是通道数增加为120。
- 第四模块:将经过第3次卷积提取到的特征图输入到全连接层。第一个全连接层的输出神经元的个数是64,第二个全连接层的输出神经元个数是分类标签的类别数,对于手写数字识别其大小是10。然后使用Softmax激活函数即可计算出每个类别的预测概率。
# 导入需要的包
import paddle
import paddle.fluid as fluid
import numpy as np
from paddle.fluid.dygraph.nn import Conv2D, Pool2D, Linear
# 定义LeNet网络,图片尺寸[28*28*1],输入[n,1,28,28]
class LeNet(fluid.dygraph.Layer):
def __init__(self, num_classes=1):
super(LeNet, self).__init__()
# 创建卷积和池化块,每个卷基层使用sigmoid激活函数,后面跟着一个2*2的池化
self.conv1 = Conv2D(num_channels=1,
num_filters=6,
filter_size=5,
act='sigmoid') # 输出:[n,6,24,24]
self.pool1 = Pool2D(pool_size=2,
pool_stride=2,
pool_type='max') # 输出:[n,6,12,12]
self.conv2 = Conv2D(num_channels=6,
num_filters=16,
filter_size=5,
act='sigmoid') # 输出:[n,16,8,8]
self.pool2 = Pool2D(pool_size=2,
pool_stride=2,
pool_type='max') # 输出:[n,16,4,4]
self.conv3 = Conv2D(num_channels=16,
num_filters=120,
filter_size=4,
act='sigmoid') # 输出:[n,120,1,1]
self.fc1 = Linear(input_dim=120,
output_dim=64,
act='sigmoid') # 输出:[n,64]
self.fc2 = Linear(input_dim=64,
output_dim=num_classes) # 输出:[n,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 = fluid.layers.reshape(x, [x.shape[0], -1])
x = self.fc1(x)
x = self.fc2(x)
return x
下面的程序使用随机数作为输入,查看经过LeNet-5的每一层作用之后,输出数据的形状
# 输入数据形状是 [N, 1, H, W]
# 这里用np.random创建一个随机数组作为输入数据
x = np.random.randn(*[3,1,28,28])
x = x.astype('float32')
with fluid.dygraph.guard():
# 创建LeNet类的实例,指定模型名称和分类的类别数目
m = LeNet(num_classes=10)
# 通过调用LeNet从基类继承的sublayers()函数,
# 查看LeNet中所包含的子层
print(m.sublayers())
x = fluid.dygraph.to_variable(x)
for item in m.sublayers():
# item是LeNet类中的一个子层
# 查看经过子层之后的输出数据形状
try:
x = item(x)
except:
x = fluid.layers.reshape(x, [x.shape[0], -1])
x = item(x)
if len(item.parameters())==2:
# 查看卷积和全连接层的数据和参数的形状,
# 其中item.parameters()[0]是权重参数w,item.parameters()[1]是偏置参数b
print(item.full_name(), x.shape, item.parameters()[0].shape, item.parameters()[1].shape)
else:
# 池化层没有参数
print(item.full_name(), x.shape)
[<paddle.fluid.dygraph.nn.Conv2D object at 0x7f3b91e66a10>, <paddle.fluid.dygraph.nn.Pool2D object at 0x7f3b91e66b90>, <paddle.fluid.dygraph.nn.Conv2D object at 0x7f3c141c4f50>, <paddle.fluid.dygraph.nn.Pool2D object at 0x7f3b91e7e110>, <paddle.fluid.dygraph.nn.Conv2D object at 0x7f3b91e7e170>, <paddle.fluid.dygraph.nn.Linear object at 0x7f3b91e7e290>, <paddle.fluid.dygraph.nn.Linear object at 0x7f3b91e7e410>]
conv2d_0 [3, 6, 24, 24] [6, 1, 5, 5] [6]
pool2d_0 [3, 6, 12, 12]
conv2d_1 [3, 16, 8, 8] [16, 6, 5, 5] [16]
pool2d_1 [3, 16, 4, 4]
conv2d_2 [3, 120, 1, 1] [120, 16, 4, 4] [120]
linear_0 [3, 64] [120, 64] [64]
linear_1 [3, 10] [64, 10] [10]
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,
padding=5,
stride=4,
act='relu') # [n,96,56,56]
self.pool1 = Pool2D(pool_size=2,
pool_stride=2,
pool_type='max') # [n,96,28,28]
self.conv2 = Conv2D(num_channels=96,
num_filters=256,
filter_size=5,
padding=2,
stride=1,
act='relu') # [n,256,28,28]
self.pool2 = Pool2D(pool_size=2,
pool_stride=2,
pool_type='max') # [n,256,14,14]
self.conv3 = Conv2D(num_channels=256,
num_filters=384,
filter_size=3,
padding=1,
stride=1,
act='relu') # [n,384,14,14]
self.conv4 = Conv2D(num_channels=384,
num_filters=384,
filter_size=3,
padding=1,
stride=1,
act='relu') # [n,384,14,14]
self.conv5 = Conv2D(num_channels=384,
num_filters=256,
filter_size=3,
padding=1,
stride=1,
act='relu') # [n,256,14,14]
self.pool5 = Pool2D(pool_size=2,
pool_stride=2,
pool_type='max') # [n,256,7,7]
self.fc1 = Linear(input_dim=12544,
output_dim=4096,
act='relu') # [n,4096]
self.drop_ratio1 = 0.5
self.fc2 = Linear(input_dim=4096,
output_dim=4096,
act='relu') # [n,4096]
self.drop_ratio2 = 0.5
self.fc3 = Linear(input_dim=4096,
output_dim=num_classes) # [n,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
VGG
# -*- coding:utf-8 -*-
# VGG模型代码
import numpy as np
import paddle
import paddle.fluid as fluid
from paddle.fluid.dygraph.nn import Conv2D, Pool2D, BatchNorm, Linear
from paddle.fluid.dygraph.base import to_variable
# 定义vgg块,包含多层卷积和1层2*2的最大池化层
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__()
# 卷基层列表,用于存储当前vgg块的卷基层
self.conv_list = []
# 定义当前vgg块的卷积层
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,
stride=1,
act='relu'))
self.conv_list.append(conv_layer)
# 上一层的输出通道数等于这一层的输入通道数
in_channels = out_channels
# 定义池化层
self.pool = Pool2D(pool_size=2,
pool_stride=2,
pool_type='max')
def forward(self,x):
for item in self.conv_list:
x = item(x)
x = self.pool(x)
return x
# 定义vgg网络
class VGG(fluid.dygraph.Layer):
def __init__(self, conv_arch=((2,64),(2,128),(3,256),(3,512),(3,512))):
super(VGG, self).__init__()
# vgg块列表,用于存储vgg块
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 = self.fc1(x)
x = fluid.layers.dropout(x, self.drop1_ratio)
x = self.fc2(x)
x = fluid.layers.dropout(x, self.drop2_ratio)
x = self.fc3(x)
return x
GoogLeNet
Inception模块结构示意图:
# -*- coding:utf-8 -*-
# GoogLeNet模型代码
import numpy as np
import paddle
import paddle.fluid as fluid
from paddle.fluid.dygraph.nn import Conv2D, Pool2D, BatchNorm, Linear
from paddle.fluid.dygraph.base import to_variable
# 定义Inception块
class Inception(fluid.dygraph.Layer):
def __init__(self, c0, c1, c2, c3, c4, **kwargs):
'''
Inception模块的实现代码,
c0,上层网络输出
c1,第一条支路1x1卷积的输出通道数,数据类型是整数
c2,第二条支路卷积的输出通道数,数据类型是tuple或list, 其中c2[0]是1x1卷积的输出通道数,c2[1]是3x3
c3,第三条支路卷积的输出通道数,数据类型是tuple或list, 其中c3[0]是1x1卷积的输出通道数,c3[1]是3x3
c4,第一条支路1x1卷积的输出通道数,数据类型是整数
'''
super(Inception, self).__init__()
# 依次创建Inception块每条支路上使用的操作
self.p1_1 = Conv2D(num_channels=c0, # 模块上一层输出通道数,第一条支路输入通道数
num_filters=c1, # 模块第一条支路输出通道数
filter_size=1,
act='relu')
self.p2_1 = Conv2D(num_channels=c0, # 模块上一层输出通道数,第二条支路第一层输入通道数
num_filters=c2[0], # 模块第二条支路,第一层输出通道数
filter_size=1,
act='relu')
self.p2_2 = Conv2D(num_channels=c2[0], # 模块第二条支路,第二层输入通道数
num_filters=c2[1], # 模块第二条支路,第二层输出通道数
filter_size=3,
padding=1,
act='relu')
self.p3_1 = Conv2D(num_channels=c0, # 模块上一层输出通道数,第三条支路,第一层输入通道数
num_filters=c3[0], # 模块第三条支路,第一层输出通道数
filter_size=1,
act='relu')
self.p3_2 = Conv2D(num_channels=c3[0], # 模块第三条支路,第二层输入通道数
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_channels=c0, # 模块上一层输出通道数,第四条支路第二层输入通道数
num_filters=c4, # 模块第四条支路,第二层输出通道数
filter_size=1,
act='relu')
def forward(self, x):
# 支路1只包含一个1*1的卷积
p1 = self.p1_1(x)
# 支路2包含1*1卷积+3*3卷积
p2 = self.p2_2(self.p2_1(x))
# 支路3包含1*1卷积+5*5卷积
p3 = self.p3_2(self.p3_1(x))
# 支路4包含最大池化+1*1卷积
p4 = self.p4_2(self.p4_1(x))
# 将每个支路的输出特征图拼接在一起作为最终的输出结果
# 对输入沿 axis 轴进行联结。
return fluid.layers.concat([p1, p2, p3, p4], # 输入是待联结的多维 Tensor 组成的 list
axis=1) # 指定对输入Tensor进行运算的轴, axis 的有效范围是[-R, R),R是输入 input 中 Tensor 的维度
# 定义GoogLeNet网络
class GoogLeNet(fluid.dygraph.Layer):
def __init__(self):
super(GoogLeNet, self).__init__()
# GoogLeNet包含五个模块,每个模块后面紧跟一个池化曾
# 第一个模块包含1个卷基层+3*3最大池化层
self.conv1 = Conv2D(num_channels=3,
num_filters=64,
filter_size=7,
padding=3,
act='relu')
self.pool1 = Pool2D(pool_size=3,
pool_stride=2,
pool_padding=1,
pool_type='max')
# 第二个模块包含2个卷基层+3*3池化层
self.conv2_1 = Conv2D(num_channels=64,
num_filters=64,
filter_size=1,
act='relu')
self.conv2_2 = Conv2D(num_channels=64,
num_filters=192,
filter_size=3,
padding=1,
act='relu')
self.pool2 = Pool2D(pool_size=3,
pool_stride=2,
pool_padding=1,
pool_type='max')
# 第三个模块包含2个Inception块+3*3最大池化
self.block3_1 = Inception(192, 64, (96, 128), (16, 32), 32)
self.block3_2 = Inception(256, 128, (128, 192), (32, 96), 64)
self.pool3 = Pool2D(pool_size=3,
pool_stride=2,
pool_padding=1,
pool_type='max')
# 第四个模块包含5个Inception块,+3*3最大池化层
self.block4_1 = Inception(480, 192, (96, 208), (16, 48), 64)
self.block4_2 = Inception(512, 160, (112, 224), (24, 64), 64)
self.block4_3 = Inception(512, 128, (128, 256), (24, 64), 64)
self.block4_4 = Inception(512, 112, (144, 288), (32, 64), 64)
self.block4_5 = Inception(528, 256, (160, 320), (32, 128), 128)
self.pool4 = Pool2D(pool_size=3,
pool_stride=2,
pool_padding=1,
pool_type='max')
# 第五个模块包含2个Inception块
self.block5_1 = Inception(832, 256, (160, 320), (32, 128), 128)
self.block5_2 = Inception(832, 384, (192, 384), (48, 128), 128)
# 全局池化,尺寸用的是global_pooling,pool_stride不起作用
self.pool = Pool2D(pool_stride=1,
global_pooling=True,
pool_type='avg')
self.fc = Linear(input_dim=1024,
output_dim=1,
act=None)
def forward(self, x):
# 第一模块使用一个64通道的7 × 7卷积层。
x = self.pool1(self.conv1(x))
# 二模块使用2个卷积层:首先是64通道的1 × 1卷积层,然后是将通道增大3倍的3 × 3卷积层。
x = self.pool2(self.conv2_2(self.conv2_1(x)))
# 第三模块串联2个完整的Inception块。
x = self.pool3(self.block3_2(self.block3_1(x)))
# 第四模块串联了5个Inception块。
x = self.pool4(self.block4_5(self.block4_4(self.block4_3(self.block4_2(self.block4_1(x))))))
# 第五模块串联了2 个Inception块。
x = self.block5_2(self.block5_1(x))
# 第五模块的后面紧跟输出层,使用全局平均池化层来将每个通道的高和宽变成1,最后接上一个输出个数为标签类别数的全连接层。
x = self.pool(x)
x = fluid.layers.reshape(x, [x.shape[0], -1])
x = self.fc(x)
return x
ResNet
# -*- coding:utf-8 -*-
# ResNet模型代码
import numpy as np
import paddle
import paddle.fluid as fluid
from paddle.fluid.dygraph.nn import Conv2D, Pool2D, BatchNorm, Linear, BatchNorm
from paddle.fluid.dygraph.base import to_variable
# 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_channels=num_filters, # 指明输入 Tensor 的通道数量。
act=act) # 应用于输出上的激活函数,如tanh、softmax、sigmoid,relu等
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__()
# 创建第一个卷基层1*1
self.conv1 = ConvBNLayer(num_channels=num_channels,
num_filters=num_filters,
filter_size=1,
act='relu')
# 创建第二个卷基层3*3
self.conv2 = ConvBNLayer(num_channels=num_filters,
num_filters=num_filters,
filter_size=3,
stride=stride,
act='relu')
# 创建第三个卷积层1*1,但输出通道数×4
self.conv3 = ConvBNLayer(num_channels=num_filters,
num_filters=num_filters*4,
filter_size=1,
act=None)
# 如果conv3的输出跟此残差块的输入数据形状一致,则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.conv1(inputs)
conv2 = self.conv2(y)
conv3 = self.conv3(conv2)
# 如果shortcut=True,直接将inputs跟conv2的输出相加
# 否则需要对inputs进行一次卷积,将形状调整成跟conv2输出一致
if self.shortcut:
short = inputs
else:
short = self.short(inputs)
# 逐元素相加算子,输入 x 与输入 y 逐元素相加,并将各个位置的输出元素保存到返回结果中。
y = fluid.layers.elementwise_add(x=short, y=conv3, act='relu')
return y
# 定义RestNet模型
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:
#ResNet152包含多个模块,其中第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,
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