『跟着雨哥学AI』系列之二:详解飞桨框架模型组网

原创
01/25 18:57
阅读数 1.5K

课程简介:

“跟着雨哥学AI”是百度飞桨开源框架近期针对高层API推出的系列课。本课程由多位资深飞桨工程师精心打造,不仅提供了从数据处理、到模型组网、模型训练、模型评估和推理部署全流程讲解;还提供了丰富的趣味案例,旨在帮助开发者更全面清晰地掌握百度飞桨框架的用法,并能够举一反三、灵活使用飞桨框架进行深度学习实践。

图片

下载安装命令

## CPU版本安装命令
pip install -f https://paddlepaddle.org.cn/pip/oschina/cpu paddlepaddle

## GPU版本安装命令
pip install -f https://paddlepaddle.org.cn/pip/oschina/gpu paddlepaddle-gpu

本章分别对框架内置模型、模型结构可视化、Sequential组网、SubClass组网以及基于内置模型的二次开发进行详细的讲解。

在上周发布的 『跟着雨哥学AI』系列:详解飞桨框架数据管道 中,已经给大家简单介绍了内置数据集、数据集定义、数据增强、数据采样以及数据加载5个功能点的使用方法。现在我们就进入令人兴奋的模型组网环节了。对于模型组网,飞桨提供了5种重要的功能,包括框架内置模型、模型结构可视化、Sequential组网、SubClass组网以及基于内置模型的二次开发。接下来我将分别对这五个功能进行详细的讲解。如果大家对哪部分有疑问,欢迎留言,我会一一解答。好的,那下面就让我们进入今天的内容吧。

什么是模型组网?

如果说数据是深度学习任务的前置准备,那么模型结构就是深度学习任务的核心架构,是不可缺少的一部分。我们常常听说模型,那么何为模型呢?我相信同学们在入门深度学习的时候遇到了许多新的概念,例如神经元、权重、偏置以及激活函数等等,这些基本单元的组成构成了各种各样的层级结构,例如卷积层、池化层等等。模型就是这些层级结构通过合理有效地组织方式组成的网络架构。一个好的模型结构在性能上可以起到关键性的作用,例如何凯明大神提出的ResNet网络结构,通过引入shortcut连接缓解深层模型结构导致的梯度消失或梯度爆炸问题,刷新了卷积神经网络模型在ImageNet上的得分。看到这里同学们是不是也想尝试一下自己动手组网了呢?不过在学习模型组网之前,我们先带同学们回忆一下刚刚提到的那些基础概念吧。

神经网络的基础概念

我们这里先简单介绍一些在任务中经常接触和使用到的基础概念,让大家有所认知和理解,便于学习接下里的实际组网代码。

1. 神经元

提到神经元,大家一定首先联想到生物的神经系统。以大脑为例,当大脑中的一个神经元接触到新的信息时,会对其进行处理,最后大脑会产生一些特定的反应和指令。和我们大脑中的基本组成单元类似,这里提到的神经元是组成神经网络的基础结构。在神经网络中,神经元在收到输入的信号之后,会进行一系列地处理,最终把结果传递给其它的神经元或者直接作为最终的输出。

2. 权重

我们知道,生物神经系统中的神经元接受到输入信号后会进行一系列的处理。在神经网络中,这些输入信号会乘以相应的权重因子。假设一个神经元有两个输入信号,那么每个输入将会存在着一个与之相应的权重因子。在初始化网络的时候,这些权重会被随机设置,在训练模型的过程中再不断地发生更改,最终生成不同的权重因子。从现实意义来说,一个输入具有的权重因子越高,往往意味着它的重要性更高,对输出的影响越大。这便是权重的意义。

3. 偏置

除了权重这一处理之外,神经网络还会对输入信号进行另外一种线性处理,叫做偏置。通过把偏置与加权后的输入信号直接相加,从而生成输出结果。

4. 激活函数

输入信号经过上述一系列处理以后,处理后的输入信号需要通过激活函数进行非线性变换,从而得到输出信号。常见的激活函数包括Sigmoid函数,线性整流函数(ReLU)和Softmax函数等,感兴趣的同学可以在网上查找相关的博客资源,这里就不做过多的赘述。

卷积神经网络基本概念

1. 卷积层

卷积层是由一组卷积核组成的,那么何为卷积核呢?卷积核可以视为一个二维数字矩阵。假设现在有一组图像作为新的数据信息输入到我们的卷积层中,那么卷积核与输入图像进行卷积操作会产生输出图像,具体的卷积操作步骤如下:
1.在图像的某个位置上覆盖卷积核;
2.将卷积核中的值与图像中的对应像素的值相乘;
3.对步骤2所有的乘积结果进行求和操作,得到的结果是输出图像中目标像素的值;
4.对图像的所有位置重复此操作。

计算过程如图所示:输入数据是尺寸为3的矩阵,卷积核尺寸为2的矩阵。把卷积核的每个元素跟其位置对应的输入数据中的元素相乘,再把所有乘积相加,得到卷积输出的第一个结果。

 图片

飞桨框架中实现卷积操作的API是paddle.nn.Conv2D(),该API可以根据输入通道数、输出通道数以及卷积核尺寸等等参数计算输出图像即输出特征层的大小。如下所示,输入数据[2,4,8,8],经过卷积核大小为3x3的卷积层,其余参数均为默认值时,会得到(6,6)的输出特征层。

import paddle
import paddle.nn as nn

paddle.__version__
'2.0.0-rc1'
x = paddle.uniform((2, 4, 8, 8), dtype='float32', min=-1., max=1.)

conv = nn.Conv2D(4, 6, (3, 3)) #卷积层输入通道数为4,输出通道数为6,卷积核尺寸为3*3
y = conv(x) #输入数据x
y = y.numpy()
print(y.shape)
(2, 6, 6, 6)

2. 池化层

池化层是当前卷积神经网络中常用网络层之一,最早可见于LeNet一文,称之为Subsample,自AlexNet之后采用Pooling命名。池化层是模仿人的视觉系统对数据进行降维,用更高层次的特征表示图像。池化层的常见操作包含很多种,如最大值池化、平均值池化、随机池化、中值池化以及组合池化等。最大值池化和平均池化是较为常见的池化操作,最大池化选图像区域的最大值作为该区域池化后的值,而平均池化计算图像区域的平均值作为该区域池化后的值。最大池化操作的优点在于它能学习到图像的边缘和纹理结构,而平均池化可以减小估计均值的偏移,提升模型的鲁棒性。

计算过程如图所示:输入数据是尺寸为4的矩阵,池化窗口是尺寸为2的矩阵。对池化窗口覆盖区域内的像素取最大值或者平均值,就可以得到相应的输出特征图的像素值。

 图片

飞桨框架中,二维最大池化操作是通过paddle.nn.MaxPool2D()实现的,该操作可以根据卷积核尺寸、步长、padding等等参数计算输出特征层的大小。如下面代码所示,我们设定一个2x2的滤波器,步长为2,以划窗的方式对输入数据进行最大池化操作,最终选取最大(MAX)的值作为第一个窗口区域池化后的值。


import numpy as np


input = paddle.uniform(shape=[1, 1, 4, 4], dtype='float32', min=-1, max=1)
print('-----------------输入的数据:------------------')
print(input.numpy)

MaxPool2D = nn.MaxPool2D(kernel_size=2,stride=2, padding=0) #2x2的滤波器,步长为2
output = MaxPool2D(input)
print('-------------经过最大池化后的数据:--------------')
print(output.numpy)


-----------------输入的数据:------------------
<bound method PyCapsule.numpy of Tensor(shape=[1, 1, 4, 4], dtype=float32, place=CUDAPlace(0), stop_gradient=True,
       [[[[ 0.41215312, -0.95926785, -0.81856787,  0.90908360],
          [ 0.37562990,  0.03282309,  0.40360177,  0.25969160],
          [-0.42918748,  0.69227231,  0.67411363,  0.13841236],
          [-0.69833356,  0.74061847,  0.39555919,  0.03817868]]]])>
-------------经过最大池化后的数据:--------------
<bound method PyCapsule.numpy of Tensor(shape=[1, 1, 2, 2], dtype=float32, place=CUDAPlace(0), stop_gradient=True,
       [[[[0.41215312, 0.90908360],
          [0.74061847, 0.67411363]]]])>



二维平均池化操作通过paddle.nn.AvgPool2D()实现,如下面代码所示,我们同样设定一个2x2的滤波器,步长为2,以划窗的方式对输入数据进行平均池化操作,最终选取平均(Avg)的值作为第一个窗口区域池化后的值。

# 二维平均池化
input = paddle.uniform(shape=[1, 1, 4, 4], dtype='float32', min=-1, max=1)
print('-----------------输入的数据:------------------')
print(input.numpy)

AvgPool2D = paddle.nn.AvgPool2D(kernel_size=2,stride=2, padding=0)
output = AvgPool2D(input)
print('-------------经过平均池化后的数据:--------------')
print(output.numpy)
-----------------输入的数据:------------------
<bound method PyCapsule.numpy of Tensor(shape=[1, 1, 4, 4], dtype=float32, place=CUDAPlace(0), stop_gradient=True,
       [[[[-0.83284581, -0.30125332,  0.20173883,  0.13822353],
          [ 0.18920112,  0.92586553,  0.45477390,  0.39289749],
          [-0.44543380,  0.46540701, -0.33561677, -0.55682969],
          [-0.72528177, -0.07691437, -0.73434561,  0.40251398]]]])>
-------------经过平均池化后的数据:--------------
<bound method PyCapsule.numpy of Tensor(shape=[1, 1, 2, 2], dtype=float32, place=CUDAPlace(0), stop_gradient=True,
       [[[[-0.00475812,  0.29690844],
          [-0.19555573, -0.30606952]]]])>

3. 线性变换层

线性层又称为全连接层,其每个神经元与上一层的所有神经元相连,实现对前一层的线性组合和线性变换。在卷积神经网络分类任务中,输出分类结果之前,通常采用全连接层对特征进行处理。在飞桨框架中,线性变换层通过paddle.nn.Linear()实现,使用操作如下所示:


x = paddle.randn((3, 6), dtype="float32")
print('-----------------输入的数据:------------------')
print(x.numpy)

linear = paddle.nn.Linear(6, 4)
y = linear(x)
print('-------------经过线性变换层后的数据:-------------')
print(y.shape)
print(y.numpy)
-----------------输入的数据:------------------
<bound method PyCapsule.numpy of Tensor(shape=[3, 6], dtype=float32, place=CUDAPlace(0), stop_gradient=True,
       [[ 2.10471106,  0.30391440, -0.76023185, -0.33831576,  0.29458281, -0.27583912],
        [-0.95177937, -0.68669844, -0.67601967, -0.54118657,  0.91707015,  0.84619856],
        [ 0.09748562,  1.01589751,  0.71198410, -1.68161333, -2.62006807, -0.91363424]])>
-------------经过线性变换层后的数据:-------------
[3, 4]
<bound method PyCapsule.numpy of Tensor(shape=[3, 4], dtype=float32, place=CUDAPlace(0), stop_gradient=False,
       [[ 1.11399877, -1.39402986,  0.68232262, -0.47672343],
        [-2.78190589, -0.19977391, -1.30184209, -0.08473665],
        [ 3.49870014,  1.11108458,  1.59574246,  1.82870936]])>

 

4. 激活函数层

激活函数层在神经网络中是最常见的一个网络结构层,该网络层是用来加入非线性因素的,为什么要加入非线性因素呢?这是由于因为线性模型的表达能力不够,通过加入非线性激活函数以后,神经网络的表达能力可以变的更加强大。常用的激活函数包括Sigmoid函数、tanh函数以及Relu函数等。下面的程序画出了Sigmoid和ReLU函数的曲线图:

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)

plt.show()

图片

<Figure size 720x360 with 2 Axes>

飞桨框架为同学们提供了多种常用的激活函数。如Relu函数,通过调用paddle.nn.ReLU(),就可以实现激活函数层。

x = paddle.to_tensor(np.array([-2, 0, 1]).astype('float32'))
m = paddle.nn.ReLU()
out = m(x) # [0., 0., 1.]
print(out)
Tensor(shape=[3], dtype=float32, place=CUDAPlace(0), stop_gradient=True,
       [0., 0., 1.])

模型组网详解

对上述基础概念进行简单地回顾后,我们就开始模型组网的部分吧。飞桨高层API在模型组网部分提供了多个功能供同学们使用,主要包括框架自带的内置模型、模型结构可视化、Sequential组网、SubClass组网以及基于内置模型的二次开发。接下来我将对以下功能进行详细的讲解。

框架内置模型

有部分未接触过深度学习的同学私信我说期望可以快速上手一些基础模型,感受模型的输入和输出具体是什么形式以及模型的整体性能,但是苦于基础理论知识的欠缺,很难将从网上下载的代码给运行起来。为了方便新手同学的学习,飞桨框架内置了16个CV领域的常见基础模型供大家使用,具体路径为paddle.vision.models,具体列表如下,有ResNet系列模型以及VGG系列模型等等。


print('飞桨框架内置模型:', paddle.vision.models.__all__)

飞桨框架内置模型:['ResNet', 'resnet18', 'resnet34', 'resnet50', 'resnet101', 'resnet152', 'VGG', 'vgg11', 'vgg13', 'vgg16', 'vgg19', 'MobileNetV1', 'mobilenet_v1', 'MobileNetV2', 'mobilenet_v2', 'LeNet']

接下来为大家展示内置的模型该如何使用,其实使用的方式也非常简单,仅通过一行代码 from paddle.vision.models import * 就可以导入相应的模型。以使用LeNet模型为例,我们首先导入LeNet模型,假设输入数据为[1,1,28,28],经过LeNet模型的处理后,得出[1,10]的Tensor。


from paddle.vision.models import LeNet

model = LeNet()

x = paddle.rand([1, 1, 28, 28])
out = model(x)
print(out)
print(out.shape)
Tensor(shape=[1, 10], dtype=float32, place=CUDAPlace(0), stop_gradient=False,
       [[ 0.60343379, -0.43607980,  1.40375721,  0.25429735,  1.51849413,  2.69145012, -0.14804429,  1.10927546, -1.32646799,  1.45668364]])
[1, 10]



模型结构可视化

在上述的实践过程中,我们举了一个LeNet模型的例子,可能有很多同学会感到疑惑,LeNet模型内部究竟是什么样的模型结构呢?是如何将输入的数据变成[1,10]的向量呢?别担心,飞桨框架也提供了模型可视化的工具,通过调用模型可视化API即可以看清所有模型的庐山真面目。那么我们开始动手实践一下吧?依然以上述的LeNet模型为例,我们调用paddle.summary()就可以查看模型的详细结构。具体操作如下:



paddle.summary(model, (64, 1, 28, 28))
---------------------------------------------------------------------------
 Layer (type)       Input Shape          Output Shape         Param #    
===========================================================================
   Conv2D-2      [[64, 1, 28, 28]]     [64, 6, 28, 28]          60       
    ReLU-2       [[64, 6, 28, 28]]     [64, 6, 28, 28]           0       
  MaxPool2D-2    [[64, 6, 28, 28]]     [64, 6, 14, 14]           0       
   Conv2D-3      [[64, 6, 14, 14]]     [64, 16, 10, 10]        2,416     
    ReLU-3       [[64, 16, 10, 10]]    [64, 16, 10, 10]          0       
  MaxPool2D-3    [[64, 16, 10, 10]]     [64, 16, 5, 5]           0       
   Linear-2         [[64, 400]]           [64, 120]           48,120     
   Linear-3         [[64, 120]]            [64, 84]           10,164     
   Linear-4          [[64, 84]]            [64, 10]             850      
===========================================================================
Total params: 61,610
Trainable params: 61,610
Non-trainable params: 0
---------------------------------------------------------------------------
Input size (MB): 0.19
Forward/backward pass size (MB): 7.03
Params size (MB): 0.24
Estimated Total Size (MB): 7.46
---------------------------------------------------------------------------

{'total_params': 61610, 'trainable_params': 61610}

如上述结果所示,我们可以看清LeNet模型包含卷积层、ReLU层、池化层以及全连接层等等,这些层通过堆叠形成了我们所使用的LeNet模型结构,且结果会非常清晰的显示每一层的输入数据和输出数据的形状,包括模型整体的参数量。可以很好的助力我们对各个模型的整体理解。是不是非常的简单?感兴趣的同学一定要亲手实践一下。

Sequential组网

以上两个小节介绍了飞桨框架内置的模型以及模型可视化的工具包。那么现在就来学习一下如何自己进行模型组网吧。为了灵活地进行模型组网,飞桨框架统一支持两种组网方式,分别是SequentialSubClass的方式进行模型的组建。我们可以根据自己实际的使用场景,来选择最合适的组网方式。当我们想组建顺序的线性网络结构时,我们可以直接使用 Sequential组网方式,相比于 SubClass 组网方式 ,Sequential 可以快速的完成组网。但是当我们想组建一些比较复杂的网络结构,我们可以使用 SubClass 定义的方式来进行模型代码编写。因此,建议新手可以尝试第一种组网方式,进阶的同学可以尝试第二种组网方式。那么接下来我们先详细学习一下第一种Sequential组网吧。

Sequential组网方式很简单,我们只需要按网络模型的结构顺序,一层一层的加到 Sequential 后面即可,非常快速就可以完成模型的组建。使用 Sequential 进行组网的实现如下。

同样,我们可以使用 summary API对我们自己组建的模型进行可视化,可以观察到可视化结果与我们的组网架构一模一样,是不是非常简单?


mnist = paddle.nn.Sequential(
    paddle.nn.Flatten(),
    paddle.nn.Linear(784, 512),
    paddle.nn.ReLU(),
    paddle.nn.Dropout(0.2),
    paddle.nn.Linear(512, 10)
)
#使用summary API对构建的模型结构进行可视化
paddle.summary(mnist, (64, 1, 28, 28))
---------------------------------------------------------------------------
 Layer (type)       Input Shape          Output Shape         Param #    
===========================================================================
   Flatten-3     [[64, 1, 28, 28]]        [64, 784]              0       
   Linear-5         [[64, 784]]           [64, 512]           401,920    
    ReLU-4          [[64, 512]]           [64, 512]              0       
   Dropout-1        [[64, 512]]           [64, 512]              0       
   Linear-6         [[64, 512]]            [64, 10]            5,130     
===========================================================================
Total params: 407,050
Trainable params: 407,050
Non-trainable params: 0
---------------------------------------------------------------------------
Input size (MB): 0.19
Forward/backward pass size (MB): 1.14
Params size (MB): 1.55
Estimated Total Size (MB): 2.88
---------------------------------------------------------------------------

{'total_params': 407050, 'trainable_params': 407050}

Note:Sequential比较适合将单输入单输出的组网API进行顺序组合,对于多输入多输出如果也想用这个方式来组网的话可以在做一个封装,将原有API根据自己的场景转变为单输入单输出。

SubClass组网

以上的Sequential组网方式比较简单,能构建一些基础的模型结构。然而,在面对一些较为复杂的场景时,这些简单的模型结构是无法应对的,因此SubClass组网方式应运而生。具体来说,我们使用Layer子类定义的方式来进行模型代码编写,在__init__构造函数中进行组网Layer的声明,在forward中使用声明的Layer变量进行前向计算。子类组网方式也可以实现sublayer的复用,针对相同的layer可以在构造函数中一次性定义,在forward中多次调用。在实现方式上,我们可以看出 SubClass 组网会比 Sequential 组网更复杂一些,但是这带来的是网络模型结构的灵活性,我们可以设计不同的网络模型结构来应对不同的场景。

我们一起动手实践一下吧,使用 SubClass 进行组网的实现如下,SubClass 组网的结果与Sequential 组网的结果完全一致,但是在Sequential组网中,如果我们想加入多层相同的网络层时,需要多次添加需要的网络层,而在SubClass组网中,我们可以通过一次定义多次调用即可,非常的方便高效。


class Mnist(paddle.nn.Layer):
    def __init__(self):
        super(Mnist, self).__init__()

        self.flatten = paddle.nn.Flatten()
        self.linear_1 = paddle.nn.Linear(784, 512)
        self.linear_2 = paddle.nn.Linear(512, 10)
        self.relu = paddle.nn.ReLU()
        self.dropout = paddle.nn.Dropout(0.2)

    def forward(self, inputs):
        y = self.flatten(inputs)
        y = self.linear_1(y)
        y = self.relu(y)
        y = self.dropout(y)
        y = self.linear_2(y)

        return y

mnist = Mnist()
#使用summary API对构建的模型结构进行可视化
paddle.summary(mnist, (64, 1, 28, 28))
---------------------------------------------------------------------------
 Layer (type)       Input Shape          Output Shape         Param #    
===========================================================================
   Flatten-5     [[64, 1, 28, 28]]        [64, 784]              0       
   Linear-7         [[64, 784]]           [64, 512]           401,920    
    ReLU-5          [[64, 512]]           [64, 512]              0       
   Dropout-2        [[64, 512]]           [64, 512]              0       
   Linear-8         [[64, 512]]            [64, 10]            5,130     
===========================================================================
Total params: 407,050
Trainable params: 407,050
Non-trainable params: 0
---------------------------------------------------------------------------
Input size (MB): 0.19
Forward/backward pass size (MB): 1.14
Params size (MB): 1.55
Estimated Total Size (MB): 2.88
---------------------------------------------------------------------------

{'total_params': 407050, 'trainable_params': 407050}

基于内置模型的二次开发

学会了以上知识我们就可以完全自主地去构建自己想要的模型结构了,但是有部分同学在构建更加复杂的模型时,需要以一些经典模型作为backbone。例如ResNet,我们可能需要用到resnet18去提取输入图像的特征,但是又不想自己重新去构建模型,那么大家该如何去做呢?大家还记得我们本章第一节描述的内置模型吗?非常棒!我们的内置模型不仅可以作为新同学的入门基础,也可以作为backbone为其他开发者提供二次开发。具体用法如下,假设我们构建一个FaceNet模型,以resnet18模型作为backbone,可以很方便的为大家节省重新构建模型的时间,提高开发效率。同学们赶快动手试一下吧!


class FaceNet(paddle.nn.Layer):
    def __init__(self, num_keypoints=15, pretrained=False):
        super(FaceNet, self).__init__()

        self.backbone = resnet18(pretrained)        
        self.outLayer1 = paddle.nn.Linear(1000, 512)        
        self.outLayer2 = paddle.nn.Linear(512, num_keypoints*2)

    def forward(self, inputs):        
         out = self.backbone(inputs)        
         out = self.outLayer1(out)        
         out = self.outLayer2(out)        
         return out



总结

本节课首先带大家回忆了神经网络的基本概念和卷积神经网络的基本概念,然后详细介绍了飞桨框架提供的5个模型组网相关的功能。到这里,同学们已经很好地掌握了模型组网的方法了,课后大家一定要尝试自己动手进行模型组网。下节课我们将进入到模型训练的实战部分了,大家如果有什么希望实现的模型或者感兴趣的趣味案例都可以在评论区留言,我们将会在后续的课程中给大家安排上哈,今天的课程到这里就结束了,我是雨哥,下节课见~

有任何问题可以在本项目中评论或到飞桨Github仓库提交Issue。

欢迎扫码加入飞桨框架高层API技术交流群

下载安装命令

## CPU版本安装命令
pip install -f https://paddlepaddle.org.cn/pip/oschina/cpu paddlepaddle

## GPU版本安装命令
pip install -f https://paddlepaddle.org.cn/pip/oschina/gpu paddlepaddle-gpu

 

图片

本文同步分享在 博客“飞桨PaddlePaddle”(CSDN)。
如有侵权,请联系 support@oschina.cn 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一起分享。

展开阅读全文
打赏
0
0 收藏
分享
加载中
更多评论
打赏
0 评论
0 收藏
0
分享
返回顶部
顶部