深度学习入门与快速实践

深度学习介绍

以深度学习为主要力量的AI浪潮徐徐展开

我们正处在一个巨变的时代,人工智能已经成为了这个时代的主题。人工智能成为第四次工业革命的核心驱动力,并将像机械化、电气化、信息化一样,最终会渗透到每一个行业的每一个角落。人工智能将重塑全球制造业竞争新格局、引爆新一轮产业革命。以1956年的达特茅斯会议(Dartmouth Conference)为起点,人工智能经过了60余年的发展,经历了逻辑推理理论和专家系统的两次繁荣,也经历过随之而来的两次寒冬。在最近的十年间,人工智能技术再次出现了前所未有的爆发性增长和繁荣期。机器学习技术尤其是深度学习技术的兴起是本次人工智能繁荣大潮的重要推动力。

近年来深度学习发展神速,在语音、图像、自然语言处理等技术上取得了前所未有的突破。本轮人工智能的快速发展离不开三大关键要素:算力数据算法
首先,大规模、高性能的云端计算硬件集群是人工智能发展的强劲引擎。目前深度学习算法模型结构复杂,规模大、参数多;模型的开发门槛很高。面对这些挑战,高性能的硬件,如GPU/FPGA/ASIC等,尤其是面向人工智能应用的专用芯片的发展,让机器学习的训练速度数倍提升。
第二,数据是推动人工智能发展的燃料,机器学习技术需要大量标注数据来作训练模型,在海量数据样本的基础上挖掘信息,得到有用的知识,从而做出决策。移动互联网、IoT物联网的发展,不止让手机这样的移动设备接入互联网,每一个音箱、每一个电视、每一个冰箱都可以接入云,获得人工智能的能力和服务。用户通过这些设备感受到AI带来的便利,实际上背后是云上提供的服务。云是AI的container,如果将AI比作电的话,云就是核电站,为各行各业提供源源不断的智能核动力。
第三,是不断推陈出新的人工智能算法突破,从卷积神经网络(Convolutional neural network,简称CNN)到递归神经网络(Recurrent neural network,简称RNN),从生成式对抗网络(Generative adversarial networks,简称GANs)到迁移学习(Transfer learning)。每一次新算法的提出或改进,带来了应用效果的大幅提升。人工智能技术在语音、图像、自然语言等众多领域的使用,推动这次人工智能大潮的持续发展。[1]

深度学习基本概念

机器善于规则、人类善于感知

机器善于计算。只要问题能够被一系列的数学规则形式化描述,那么通过编程的方法就可以进行运算,例如求解线性方程组,求解行星的运动轨迹。如果这些让人类手算的话会非常痛苦而且容易出错。为了尽量模拟人类的行为,人们开始研究机器学习。然而传统人工智能依赖科学家输入的规则模型,它只有在解决一些规则比较清楚的问题时才比较有效,比如击败卡斯帕罗夫的“深蓝”就是这样一种“人工智能”。但是对于直觉性问题,例如分辨图片中的是猫还是狗,从不同的口音中听出对方的实际想表达的意思,通过语气来表达自己的情绪,这些对于人来说非常容易的事情,对于传统人工智能来说就非常困难。因为这种认知类问题只有一个模糊的概念,没有清楚简单的规则。
[2]

人工智能的目标就是去解决人容易执行,但是很难形式化描述的任务。相对于传统的机器学习方法,深度学习方法是用来解决直觉问题的好方法。深度神经网络大大优化了机器学习的效果,使人工智能技术获得了突破性进展。在此基础上,图像识别、语音识别、机器翻译等都取得了长足进步。语音输入比打字快得多,机器翻译让我们基本可以看懂一篇外文资讯,图像识别则早已可以凭借一张少年时期的照片就在一堆成人照片中准确找到这个人,甚至可以把很模糊的照片恢复成清晰且准确的照片。神经网络的特点就是它不需要人类提前告知规则,它会自己从海量的基础数据里识别模式(规则)。[3]

深度学习与神经网络

深度学习技术是构建在传统神经网络技术基础之上的。可以粗略的认为当传统神经网络够“深”的时候就是深度学习了。图中的网络的层次比较少,也就是这个网络比较浅,可以想象当网络层次达到几十层上百层的时候,那么这就是一个深层次网络了。用深层次网络解决问题就是深度学习技术。

人工神经网络是受到生物神经网络的启发而发明的。生物神经网络是由无数个神经元相互连接进而组成的一个复杂网络。人工神经网络模拟了这个过程,在逻辑上我们模拟出很多的神经元,然后让这些神经元联系成网络。在深度学习领域,通常用神经网络这个词来指代人工神经网络。这里要强调一下:人工神经网络和生物神经网络仅仅是启发关系,内部原理是不同的,因为我们人类目前还没有把生物神经网络的内在全部机制完全探明。

复杂的神经系统是由简单的神经元相连组成。神经元就像图中这样,枝枝杈杈很多,看上去一边比较粗大一边比较纤细。粗大的这边的结构叫做轴突,纤细这边叫做树突。信号就是由一个细胞的轴突通过突触将信号传递给另一个细胞的树突。当无数个这样的神经元联系在一起的时候就构成了神经网络。

神经元

生物神经网络的最小单元是神经元,而人工神经网络的单元是感知机。生物的神经元是一端接受化学信号,经过加工从另一端释放出来。而感知机是一端接受一个向量作为输入,经过一定加工,另一端释放出去一个标量。

而一个神经元内部事实上经历了两个运算过程,线性运算和非线性运算。这两个运算的复合就构成了一个神经元的运算过程。线性运算就是最简单的加权求和,而非线性运算所使用的函数也被称作激活函数或者激励函数。关于激活函数在后面还会继续讨论。注意,在线性运算中的向量 w 和 偏置 b 就是该网络的参数。其中,w 表示权重, b 表示偏置。

输入层、输出层、隐藏层、全连接网络

通常一个深度学习网络就是一个多层的神经网络,那么很自然就会想到,多深算“深”呢?这个度量标准目前还没有定论,有的学者认为超过3层的为深层网络,有的学者认为超过5层为深层网络。无论多深,网络所具有的基本结构的是一致的。输入数据的那层别称作输入层,输出预测结果的一层(也就是网络的最后一层)被称作输出层,而夹在两层之间的层次被称作隐藏层。输入层通常不算做网络的一部分。如果网络中的每一个神经元都和上层的所有神经元相连也和下一层的所有神经元相连,那么这样的网络就被称作全连接网络。有时,我们也把全连接网络认为是一种经典的深度神经网络(Deep Neural Networks,DNN)。通常情况下,谈及DNN就是指全连接网络(除了DNN网络还有CNN、RNN等网络结构)。下图就是一个4层的全连接网络。

神经网络运算三部曲

神经网络运算由三步过程组成:正向传播反向传播梯度下降
正向传播的主要目的:计算预测值。反向传播的主要目的:得到参数的偏导数(也就是 w 和 b 的偏导数)。梯度下降的主要目的:更新参数,进而将损失函数的函数值尽量降低。

正向传播
主要过程为:采集客观数据 x。将 x 输入网络,从输入层开始一层一层的计算到输出层,这一路下来会得到预测值 y ^ \hat{y} y^

反向传播
主要过程为:在正向传播得到预测值 y ^ \hat{y} y^ 后,用该值与采集回来的标签值 y 做比较,进而度量预测值的准确性,如果 y ^ \hat{y} y^ 与 y 的差距很小,那么说明预测的较为精准,如果 y ^ \hat{y} y^ 与 y 的差距很大,说明预测的不准确。这个用来度量 y ^ \hat{y} y^ 与 y 的差距的函数称为损失函数。关于损失函数,后面会继续讨论。由损失函数做为起点,沿着网络反向传播求得每个参数(w和b)的偏导数的过程,称为反向传播过程。

梯度下降
主要过程为:在得到了所有参数的偏导数后,也就得到了梯度,沿着梯度反方向前进一小步,也就是向损失函数最小值的方向更靠近了一步。这个最小化的过程也被称作优化。具体的优化策略有很多种,这里不再展开详述。

激活函数

一个神经元中包含两个运算过程:线性运算和非线性运算。线性运算就是固定的(写死的)的加权求和,而非线性运算过程是用户可配置的。常用的线性激活函数有三种:sigmoid、tanh 和 relu。下面分别介绍一下这三种激活激活函数。
Sigmoid 函数主要作用就是把某实数映射到区间(0,1)内,观察图像会发现Sigmoid函数可以很好的完成这个工作,当值较大时,趋近于1,当值较小时,趋近于0。sigmoid早期最流行的激活函数,激活函数的代名词

Tanh 函数 也称为双切正切函数,范围在-1到1之间,随着x的增大或减小,函数趋于平缓,导函数趋近于0。sigmoid 函数 和 tanh 函数都可以用来做二分类问题的输出层的激活函数。可以用做网络内部神经元的激活函数。

Relu 函数。由于 sigmoid 函数 和 tanh 函数的都有饱和区(导数很小区域 0 的位置成为饱和区,观察函数图像可以很容易发现,当 x 大于 5 函数的导数就趋于 0 了)。所以,在反向传播的时候很容发生偏导数非常小的现象,那么进而会导致在梯度下降的时候,参数(w和b)更新速度过于缓慢的现象。为了克服这种现象,于是有了 Relu 函数。事实上,relu 函数是目前使用最多的激活函数。如果开发者不知道该配置什么样的激活函数比较好,那么就可以尝试配置 relu 函数。


标准 Relu 函数也不是完美的,可以观察到 Relu 在拐点处不可导,并且在 x 小于 0 的时候函数值为0,这样在反向传播的时候,就会把信息丢失。于是就又有了 Relu 的变种:LeakyRelu、参数化Relu、随机化Relu等,这里不再展开,但是目前在工业上用的最多的还是标准 Relu。

损失函数

需要定义一个损失函数(Loss function or Error function)用于对参数 w 和 b 进行优化,而损失函数的选择需要具体问题具体分析,在不同问题场景下采用不同的函数。常见的损失函数为均方误差损失函数交叉熵损失函数
均方误差损失函数
回归主要解决的是对具体数值的预测,解决回归问题的神经网络一般只有一个输出节点,这个节点的输出值就是预测值。对于回归问题,最常用的损失函数是均方误差(MSE),定义为:

交叉熵损失函数
交叉熵是一个信息论中的概念,它原来是用来估算平均编码长度的。给定两个概率分布 p 和 q,通过 q 来表示 p 的交叉熵为:

注意,交叉熵刻画的是两个概率分布之间的距离,或可以说它刻画的是通过概率分布q来表达概率分布p的困难程度,p代表正确答案,q代表的是预测值,交叉熵越小,两个概率的分布约接近。[4]
通常交叉熵损失函数是和激活函数 softmax 共同使用(也就是将 softmax 设置为输出层的激活函数)。关于 softmax 函数相关内容这里就不再展开。

PaddlePaddle 工具介绍

做深度学习是一个复杂的过程。理论上,开发者可以自己使用 Python 的 numpy 库,自己从头开始编写网络结构、正向传播过程、反向传播过程、梯度下降的优化过程、以及对GPU、FPGA、NPU的底层适配优化等工作。但是,自己来做这些事情实在工作量太过庞大,会把自己宝贵的时间浪费在底层代码的编写。作为开发者,首要任务是完成自己的业务逻辑,让自己的公司的业务尽快上线尽早尽好的为客户提供服务。所以,很多大厂和前辈编写很多非常棒的深度学习框架,使得开发者能够聚焦于自己的业务。目前主流的框架有 PaddlePaddle、TensorFlow、Pythorch 等,其中 PaddlePaddle 是一个易学、易用的开源深度学习框架,能够让开发者和企业安全、高效地实现自己的AI想法[5]。PaddlePaddle 是百度开发、开放、开源的。

使用深度学习实现房价预测----数据准备

数据集介绍

我们使用从UCI Housing Data Set(http://paddlemodels.bj.bcebos.com/uci_housing/housing.data)获得的波士顿房价数据集进行模型的训练和预测。下面的散点图展示了使用模型对部分房屋价格进行的预测。其中,每个点的横坐标表示同一类房屋真实价格的中位数,纵坐标表示线性回归模型根据特征预测的结果,当二者值完全相等的时候就会落在虚线上。所以模型预测得越准确,则点离虚线越近。

这份数据集共506行,每行包含了波士顿郊区的一类房屋的相关信息及该类房屋价格的中位数。其各维属性的意义如下:

属性名 解释 类型
CRIM 该镇的人均犯罪率 连续值
ZN 占地面积超过25,000平方呎的住宅用地比例 连续值
INDUS 非零售商业用地比例 连续值
CHAS 是否邻近 Charles River 离散值,1=邻近;0=不邻近
NOX 一氧化氮浓度 连续值
RM 每栋房屋的平均客房数 连续值
AGE 1940年之前建成的自用单位比例 连续值
DIS 到波士顿5个就业中心的加权距离 连续值
RAD 到径向公路的可达性指数 连续值
TAX 全值财产税率 连续值
PTRATIO 学生与教师的比例 连续值
B 1000(BK - 0.63)^2,其中BK为黑人占比 连续值
LSTAT 低收入人群占比 连续值
MEDV 同类房屋价格的中位数 连续值

数据预处理

连续值与离散值
观察一下数据,我们的第一个发现是:所有的13维属性中,有12维的连续值和1维的离散值(CHAS)。离散值虽然也常使用类似0、1、2这样的数字表示,但是其含义与连续值是不同的,因为这里的差值没有实际意义。例如,我们用0、1、2来分别表示红色、绿色和蓝色的话,我们并不能因此说“蓝色和红色”比“绿色和红色”的距离更远。所以通常对一个有dd个可能取值的离散属性,我们会将它们转为dd个取值为0或1的二值属性或者将每个可能取值映射为一个多维向量。不过就这里而言,因为CHAS本身就是一个二值属性,就省去了这个麻烦。

属性的归一化
另外一个稍加观察即可发现的事实是,各维属性的取值范围差别很大(例如下图)。

例如,属性B的取值范围是[0.32, 396.90],而属性NOX的取值范围是[0.3850, 0.8170]。这里就要用到一个常见的操作-归一化(normalization)了。归一化的目标是把各位属性的取值范围放缩到差不多的区间,例如[-0.5,0.5]。这里我们使用一种很常见的操作方法:减掉均值,然后除以原取值范围。

做归一化至少有以下3个理由:

  1. 过大或过小的数值范围会导致计算时的浮点上溢或下溢。
  2. 不同的数值范围会导致不同属性对模型的重要性不同(至少在训练的初始阶段如此),而这个隐含的假设常常是不合理的。这会对优化的过程造成困难,使训练时间大大的加长。
  3. 很多的机器学习技巧/模型(例如L1,L2正则项)都基于这样的假设:所有的属性取值都差不多是以0为均值且取值范围相近的。

划分训练集测试集

通常情况下,开发者在对数据集进行过必要的预处理之后,就需要讲数据集分为两部分:训练集测试集

  • 训练集:用于调整模型的参数,即进行模型的训练。
  • 测试集:用来测试,查看模型效果,用来模拟如果模型上线后的效果。
    那么有多少数据应该放入训练集呢,又有多少数据应该放入测试集呢?分割数据的比例要考虑到两个因素:更多的训练数据会降低参数估计的方差,从而得到更可信的模型;而更多的测试数据会降低测试误差的方差,从而得到更可信的测试误差。在数据集的量不大的情况下,这个比例通常是7:3或者8:2。在这个例子中我们设置的分割比例为8:2。

在更复杂的模型训练过程中,我们往往还会多使用一种数据集:验证集。因为复杂的模型中常常还有一些超参数(Hyperparameter)需要调节,所以我们会尝试多种超参数的组合来分别训练多个模型,然后对比它们在验证集上的表现选择相对最好的一组超参数,最后才使用这组参数下训练的模型在测试集上评估测试误差。由于本章训练的模型比较简单,我们暂且忽略掉这个过程。

训练误差与测试误差

此外,训练误差和测试误差也是开发者必须了解的概念:

  • 模型在训练集上的误差被称为训练误差
  • 模型在测试集上的误差被称为测试误差
    我们训练模型的目的是为了通过从训练数据中找到规律来预测未知的新数据,所以测试误差是更能反映模型表现的指标。

使用深度学习实现房价预测----上代码

代码概览

深度学习过程事实上可以划分为两个过程:训练和预测(预测也称作推理)。通常这两个过程不是在同一台机器上完成更不会在同一个进程中完成,但是,为了讲解方便代码简便,我们下面的代码是在同一个进程中顺序执行。
在本例中,我们将代码分为了3个部分:通用内容准备、训练过程代码、预测过程代码

通用内容准备

  1. 引入需要使用到的库
import paddle
import paddle.fluid as fluid
import numpy
import math
import sys
from __future__ import print_function
  1. 准备数据集
BATCH_SIZE = 20		#定义 mini-batch 的大小

train_reader = paddle.batch(
    paddle.reader.shuffle(
        paddle.dataset.uci_housing.train(), buf_size=500),		# 用来打乱顺序的 buffer 的大小为 500
    batch_size=BATCH_SIZE)

test_reader = paddle.batch(
    paddle.reader.shuffle(
        paddle.dataset.uci_housing.test(), buf_size=500),
    batch_size=BATCH_SIZE)

我们通过 uci_housing 模块引入了数据集合 UCI Housing Data Set

其中,在uci_housing模块中封装了:
数据下载的过程。下载数据保存在~/.cache/paddle/dataset/uci_housing/housing.data。
数据预处理的过程。
接下来我们定义了用于训练的数据提供器。提供器每次读入一个大小为BATCH_SIZE的数据批次。如果用户希望加一些随机性,它可以同时定义一个批次大小和一个缓存大小。这样的话,每次数据提供器会从缓存中随机读取批次大小那么多的数据。

  1. 定义网络拓扑结构
x = fluid.layers.data(name='x', shape=[13], dtype='float32')
y = fluid.layers.data(name='y', shape=[1], dtype='float32')
y_predict = fluid.layers.fc(input=x, size=1, act=None)

训练程序的目的是定义一个训练模型的网络结构。对于线性回归来讲,它就是一个从输入到输出的简单的全连接层。更加复杂的结构,比如卷积神经网络,递归神经网络这里不再展开。

main_program = fluid.default_main_program()
startup_program = fluid.default_startup_program()

这两句的作用是获得 default_main_program 和 default_startup_program 的 Python 级别的引用。default_startup_program 的主要作用是初始化参数,而 default_main_program 的主要作用是管理所有的变量和算子。作为初学者理解到这一层已经足够,PaddlePaddle 内部的机制非常复杂,该框架对开发者仅仅暴露了很少的部分,复杂的机制都包裹在后台。由于 PaddlePaddle 是开源的,有兴趣深入了解其框架底层实现的开发者可以访问其 github (https://github.com/PaddlePaddle/Paddle)。

cost = fluid.layers.square_error_cost(input=y_predict, label=y)
avg_loss = fluid.layers.mean(cost)

训练程序必须返回平均损失作为第一个返回值,因为它会被后面反向传播算法所用到。

  1. 定义优化方式
sgd_optimizer = fluid.optimizer.SGD(learning_rate=0.001)
sgd_optimizer.minimize(avg_loss)

#clone a test_program
test_program = main_program.clone(for_test=True) 

这里使用了随机梯度下降(SGD)作为优化器 ,learning_rate 是训练的速度,与网络的训练收敛速度有关系。test_program=main_program.clone(for_test=True) 的作用是为后面边训练边测试做准备(把环境拷贝了一份出来)

  1. 定义运算场所
use_cuda = False
place = fluid.CUDAPlace(0) if use_cuda else fluid.CPUPlace()
exe = fluid.Executor(place)

我们可以定义运算是发生在CPU还是GPU

# Plot data
from paddle.utils.plot import Ploter

train_prompt = "Train cost"
test_prompt = "Test cost"
plot_prompt = Ploter(train_prompt, test_prompt)

展现运算过程有两种方式:1.打印 log 的方式 2.绘制图像的方式。相比较来说,绘制图像的方式更加直观。上面的代码就是为绘制图像做准备。

训练过程代码

  1. 定义训练轮数和 train_test 函数
num_epochs = 100	#设置训练的轮数

# 在训练过程中打印出测试的 loss
def train_test(executor, program, reader, feeder, fetch_list):
    accumulated = 1 * [0]
    count = 0
    for data_test in reader():
        outs = executor.run(program=program,
                            feed=feeder.feed(data_test),
                            fetch_list=fetch_list)
        accumulated = [x_c[0] + x_c[1][0] for x_c in zip(accumulated, outs)]
        count += 1
    return [x_d / count for x_d in accumulated]

训练需要有一个训练程序和一些必要参数,并构建了一个获取训练过程中测试误差的函数

  1. 训练主循环
%matplotlib inline
# Specify the directory to save the parameters
params_dirname = "fit_a_line.inference.model"
feeder = fluid.DataFeeder(place=place, feed_list=[x, y])
naive_exe = fluid.Executor(place)
naive_exe.run(startup_program)
step = 0

exe_test = fluid.Executor(place)

# main train loop.
for pass_id in range(num_epochs):
    for data_train in train_reader():
        avg_loss_value, = exe.run(main_program,
                                  feed=feeder.feed(data_train),
                                  fetch_list=[avg_loss])
        if step % 10 == 0:  # record a train cost every 10 batches
            plot_prompt.append(train_prompt, step, avg_loss_value[0])
            plot_prompt.plot()
        if step % 100 == 0:  # record a test cost every 100 batches
            test_metics = train_test(executor=exe_test,
                                     program=test_program,
                                     reader=test_reader,
                                     fetch_list=[avg_loss.name],
                                     feeder=feeder)
            plot_prompt.append(test_prompt, step, test_metics[0])
            plot_prompt.plot()
            # If the accuracy is good enough, we can stop the training.
            if test_metics[0] < 10.0:
                break

        step += 1

        if math.isnan(float(avg_loss_value[0])):
            sys.exit("got NaN loss, training failed.")
    if params_dirname is not None:
        # We can save the trained parameters for the inferences later
        fluid.io.save_inference_model(params_dirname, ['x'],
                                      [y_predict], exe)

PaddlePaddle 提供了读取数据者发生器机制来读取训练数据。读取数据者会一次提供多列数据,因此我们需要一个 Python 的 list 来定义读取顺序。我们构建一个循环来进行训练,直到训练结果足够好或者循环次数足够多。 如果训练顺利,可以把训练参数保存到 params_dirname。

预测过程代码

需要构建一个使用训练好的参数来进行预测的程序,训练好的参数位置在params_dirname

  1. 准备预测环境
infer_exe = fluid.Executor(place)
inference_scope = fluid.core.Scope()

类似于训练过程,预测器需要一个预测程序来做预测。我们可以稍加修改我们的训练程序来把预测值包含进来。

  1. 预测
with fluid.scope_guard(inference_scope):
    [inference_program, feed_target_names,
     fetch_targets] = fluid.io.load_inference_model(params_dirname, infer_exe)
    batch_size = 10

    infer_reader = paddle.batch(
        paddle.dataset.uci_housing.test(), batch_size=batch_size)

    infer_data = next(infer_reader())
    infer_feat = numpy.array(
        [data[0] for data in infer_data]).astype("float32")
    infer_label = numpy.array(
        [data[1] for data in infer_data]).astype("float32")

    assert feed_target_names[0] == 'x'
    results = infer_exe.run(inference_program,
                            feed={feed_target_names[0]: numpy.array(infer_feat)},
                            fetch_list=fetch_targets)

    print("infer results: (House Price)")
    for idx, val in enumerate(results[0]):
        print("%d: %.2f" % (idx, val))

    print("\nground truth:")
    for idx, val in enumerate(infer_label):
        print("%d: %.2f" % (idx, val))

通过fluid.io.load_inference_model,预测器会从params_dirname中读取已经训练好的模型,来对从未遇见过的数据进行预测。

完整代码请参考:
https://github.com/PaddlePaddle/book/blob/develop/01.fit_a_line/train.py
更多 PaddlePaddle 示例请参考:
https://github.com/PaddlePaddle/book/

参考资料

[1]《PaddlePaddle深度学习实战》 刘祥龙等著 2018
[2]《白话深度学习与TensorFlow》 高扬等著,万娟 绘 2017
[3]《深度学习实战》杨云等著 2017
[4] https://en.wikipedia.org/wiki/Cross_entropy
[5] http://www.paddlepaddle.org/zh
图片部分来源于网络,侵删

你可能感兴趣的:(PaddlePaddle,深度学习)