飞桨PaddlePaddle 零基础入门深度学习课程学习总结1(week_1)

手写数字识别

作为深度学习的Hello World, 手写数字识别任务是最基础的深度学习任务之一。在之前的波士顿房价预测任务中,我们采用最基本的多元线性回归模型,也可以理解为激活函数为Linear的单层神经网络模型去训练数据,得到了不错的结果。但是,如果我们仍然将这个基本模型运用在我们的手写数字识别任务上,我们会发现,效果并不理想。所以接下来,我们从模型训练的各个流程去思考如何优化。

在课程中,毕然老师总结了完整的机器学习/深度学习模型构建的5个步骤,分别是:
1.数据处理 -->2.模型设计 -->3.训练配置 -->4.训练过程 -->5.模型保存

1.首先,我们从数据处理入手来思考优化点。
一般来说,我们的数据处理涉及5个环节,分别是读入数据,划分数据集,生成批次数据,训练样本集乱序以及校验数据有效性。

我想重点说一说划分数据集以及训练样本乱序,生成批次数据的环节。
首先划分数据集,主要分为3部分,训练集,验证集以及测试集。这里想主要说一下为什么需要验证集,验证集的存在主要用于模型超参数的调节来比较不同模型的表现,从而选出最优模型。在课程中,毕然老师讲到一个案例让我印象深刻。这里直接引用课程中的原始文段

扩展阅读:为什么学术界的模型总在不断精进呢?
通常某组织发布一个新任务的训练集和测试集数据后,全世界的科学家都针对该数据集进行创新研究,随后大量针对该数据集的论文会陆续发表。论文1的A模型声称在测试集的准确率70%,论文2的B模型声称在测试集的准确率提高到72%,论文N的X模型声称在测试集的准确率提高到90%

然而这些论文中的模型在测试集上准确率提升真实有效么?我们不妨大胆猜测一下。

假设所有论文共产生1000个模型,这些模型使用的是测试数据集来评判模型效果,并最终选出效果最优的模型。这相当于把原始的测试集当作了验证集,使得测试集失去了真实评判模型效果的能力,正如机器学习领域非常流行的一句话:“拷问数据足够久,它终究会招供”。

然后关于训练样本乱序以及生成批次数据的部分,课程中主要用到的就是Python 生成器,有效减少内存。
以下是我自己总结后的代码。

import pandas as pd
import numpy as np
import random

data=pd.read_csv('digits_data.csv')
data.shape

#1.获取总数据量
data_size=data.shape[0]
#2.生成索引列表
index_list=[i for i in range(data_size)]
#3.打乱索引
random.shuffle(index_list)
index_list,len(index_list)

#每次随机获取100个数据(随机体现在之前的radnom.shuffle())
batch_size=100
#构建生成器
def data_generator():
    feat_list=[]
    label_list=[]
    for i in index_list:
        feat_list.append(np.array(data.iloc[i,1:]).reshape(8,8))
        label_list.append(np.array(data.iloc[i,0]))

        if len(feat_list)==batch_size:
            yield np.array(feat_list),np.array(label_list).reshape(-1,1) #每次积累并生成batch_size大小的数据后,返回

            #清空上一轮batch_size的数据,进入下一轮数据生产
            feat_list=[]
            label_list=[]

    #最后一批数据可能不满足batch_size的大小,我们也直接生成产出
    if len(feat_list)<batch_size:
        yield np.array(feat_list),np.array(label_list).reshape(-1,1)

    return None
#如果我们直接打印data_generator(),只是生成一个生成器对象
print(data_generator())

#通过for循环迭代来不断读取生成器产生的批量数据
for batch_id,batch_data in enumerate(data_generator()):
    feat_data,label_data=batch_data

    #我们打印一下第一个batch数据的 特征维度 和 标签维度
    if batch_id==0:
        print('第一个batch数据的特征维度为{},标签维度为{}'.format(feat_data.shape,label_data.shape))
        break

最后,我想提的一点是课程中提到的数据异步读取。异步数据读取相较于同步数据读取的区别在于,在异步读取的过程中,数据读取与模型训练并行,读取到的数据不断放入缓冲区,不需要再等待模型训练,同样的,对于模型训练而言,不需要等待下一批数据读取完再拿数据训练,而是直接从缓冲区拿下一批训练数据。当我们的数据量规模巨大时,异步读取数据才会带来显著的性能提升。

2.接下来,我们开始进入模型设计的优化,也是对于模型预测能力提升最重要的一环。
我们已经发现,用简单的线性模型无法很好地解决手写数字的识别。那原因是什么呢?最主要的一点原因是我们的特征输入不再是一个一维的向量,而是一个二维的包含像素值的矩阵。而二维数据的特征不仅反映在值的大小,也与位置有关。简单的线性模型显然无法捕捉到这一点,因此单层线性神经网络的效果差也是意料之中的。所以,更为复杂的模型是必要的。

最直觉的想法,既然单层的线性模型不可以,那么我们就用多层的非线性模型来试一试。这也就引出了深度学习中经典的全连接神经网络。简单来说,在原始的输入层与输出层之间增加多个隐含层,每层之间(包括输入和输出层)采用全连接的方式。但是这样只是单纯增加了模型的复杂度,模型的本质依然是一个线性的模型,那么如何引入非线性呢?最基本的方法就是引入非线性的激活函数。具体来说,就是,隐含层的某个神经元在在接收前一层的线性信号(w*X+b)后并不直接传递给下一层,而是先经过一个非线性变换,也就是我们所说的激活函数后再传递。我们早期常用的也是最经典的叫做Sigmoid函数,除此之外,目前比较流行的还有像Relu函数等等,它们之间的比较以及优缺点在这里先不详细展开了,在我之后的博文中会专门介绍。总结来说,通过增加隐含层以及非线性激活函数,我们可以将简单的单层线性神经网络拓展成为经典的全连接神经网络。

在百度飞桨的框架下,要构建这种神经网络是非常简单的,直接调用API即可,下面展示基本代码。(当然需要先安装paddle库)

import paddle
import paddle.fluid as fluid
from paddle.fluid.dygraph.nn import Linear
#构建包含2个隐含层的全连接非线性神经网络
class MNIST(fluid.dygraph.Layer):
    def __init__(self):
        super(MNIST, self).__init__()
        # 定义两层全连接隐含层 fc1 和 fc2,输出维度是10,激活函数为sigmoid
        self.fc1 = Linear(input_dim=784, output_dim=10, act='sigmoid') 
        self.fc2 = Linear(input_dim=10, output_dim=10, act='sigmoid')
        # 定义一层全连接输出层,输出维度是1,不使用激活函数
        self.fc3 = Linear(input_dim=10, output_dim=1, act=None)

OK, 除了基本的全连接神经网络,我们还有其他的网络吗?回到我们的问题,我们的目标是手写数字识别,也就是处理视觉的问题。那实际上,目前在处理计算机视觉方面,还有一个非常流行的神经网络,它就是卷积神经网络(CNN)。这里我对它做一个大概的解读,具体的介绍会在我之后的博文中详细展开。相较于传统的神经网络,我们把样本的原始特征直接丢到输入层,然后训练复杂的网络,CNN会在原始特征丢入全连接网络之前,进行2个步骤的加工,它们分别叫做卷积和池化。简单来说,卷积层负责对原始输入进行多维度的扫描,提取不同的特征,每一个特征形成一个feature map,根据我们预先设定的特征个数,形成一个feature maps.池化层则是对卷积层生成的每个feature map 作过滤或者说叫压缩处理,只保留最关键的特征信息。经过多次的卷积和池化处理,我们对最终的结果做flattern处理,扩展成一维数组作为全连接网络的输入。

下面同样展示在PaddlePaddle框架下的模型构建过程,注意,我们共经过两轮卷积和池化。

from paddle.fluid.dygraph.nn import Conv2D, Pool2D, Linear
class MNIST(fluid.dygraph.Layer):
     def __init__(self):
         super(MNIST, self).__init__()
         # 定义卷积层,输出特征个数num_filters设置为20,卷积核的大小filter_size为5,卷积步长stride=1,padding=2,激活函数使用relu
         self.conv_1 = Conv2D(num_channels=1, num_filters=20, filter_size=5, stride=1, padding=2, act='relu')
         # 定义池化层,池化核pool_size=2,池化步长为2,选择最大池化方式
         self.pool_1 = Pool2D(pool_size=2, pool_stride=2, pool_type='max')
         # 定义卷积层,输出特征通道num_filters设置为20,卷积核的大小filter_size为5,卷积步长stride=1,padding=2,激活函数同样使用relu
         self.conv_2 = Conv2D(num_channels=20, num_filters=20, filter_size=5, stride=1, padding=2, act='relu')
         # 定义池化层,池化核pool_size=2,池化步长为2,选择最大池化方式
         self.pool_2 = Pool2D(pool_size=2, pool_stride=2, pool_type='max')
         # 定义一层全连接层,输出维度是1,不使用激活函数
         self.fc = Linear(input_dim=980, output_dim=1, act=None)

OK,上面我从优化网络结构的角度优化了模型,那么还有其他角度吗?当然是有的,对于数字识别这样的分类问题,我们到目前为止仍然在使用均方误差来作为loss function。但是当我们仔细思考,我们会发现一些问题。最直观的一点是我们的模型输出是一个实数,但是我们的target呢?是一个离散变量,一个标签,两者做差本身就不太合理。其次我们从分类问题的本质来思考,我们的目标是确定某张图片属于哪个标签,那我们最直观的认知应该是,我们的输出应该为这张图片属于每个标签的概率,然后把它归类为输出概率最大的一类,那么如何把实数域上的值转化概率输出呢?(本例中的多分类问题)答案就是通过一个softmax函数,在手写数字识别例子中,我们有10个标签,我们修改最后的全连接层,输出10维向量,每个向量其实对应着一个标签,经过softmax函数,将它们转化为10个概率,且概率和为1,就是这么简单。
事实上,softmax函数就是sigmoid函数在多类别下的拓展,sigmoid函数针对二元分类问题,而softmax函数解决多元分类问题。

说到这里,我们只是说到对于分类问题,我们的输出应该是每个可能标签的概率以及如何如何通过sigmoid或softmaxh函数将实数转化为概率。其实我还是没有讲到优化的核心,loss function。那么接下来,我们进入这一部分。之前,我们一直使用的是均方误差,但是对于分类问题,从统计学的角度来说,如果标签是一个离散变量,那么我们建模的选择其实就是伯努利分布(0 或 1)或多项式分布(0,1,2…K)。然后通过最大似然的思想来估计参数。对应到机器学习,其实我们也有专门的损失函数,那就是交叉熵。关于交叉熵的推导以及它与统计学最大似然的联系我会在之后的博文中专门介绍,那这里我想强调的一点就是当我们使用sigmoid函数进行机器学习建模时,比如逻辑斯蒂回归,使用交叉熵替代均方误差在训练时会有极大的好处,具体的原因我也会在之后的文章中详细解释。

具体的代码在飞桨框架中也是非常简单,只需要改动loss funciton 即可。

loss = fluid.layers.square_error_cost(predict, label)

loss = fluid.layers.cross_entropy(predict, label)

总结一下,这篇文章主要介绍了从数据处理以及模型设计的角度对我们的baseline模型进行优化,用于手写数字识别任务,在下一篇文章中,我还会继续从剩余角度总结模型优化的点。

参考:飞桨深度学习学院–百度架构师手把手带你零基础实践深度学习课程

猜你喜欢

转载自blog.csdn.net/weixin_44607838/article/details/108001950