百度飞桨-手把手带你零基础实践深度学习-课程笔记(一)

人工智能、机器学习、深度学习的关系

 概括来说,人工智能、机器学习和深度学习覆盖的技术范畴是逐层递减的。人工智能是最宽泛的概念。机器学习是当前比较有效的一种实现人工智能的方式。深度学习是机器学习算法中最热门的一个分支,近些年取得了显著的进展,并替代了大多数传统机器学习算法。
三者的关系如 图1 所示,即:人工智能 > 机器学习 > 深度学习。
百度飞桨-手把手带你零基础实践深度学习-课程笔记(一)_第1张图片
如字面含义,人工智能是研发用于模拟、延伸和扩展人的智能的理论、方法、技术及应用系统的一门新的技术科学。由于这个定义只阐述了目标,而没有限定方法,因此实现人工智能存在的诸多方法和分支,导致其变成一个“大杂烩”式的学科。

机器学习

 机器学习:机器学习(Machine Learning, ML)是一门多领域交叉学科,涉及概率论,统计学,逼近论,凸分论,算法复杂度理论等多门学科。专门研究计算机怎样模拟或实现人类的学习行为,以获取新的知识或技能。
 简单来说,机器学习就是在数据中挖掘潜在的规律,以便在下次遇到相似的数据时进行分析。就像我们上学时要做大量的练习题来巩固知识点和知识点之间的联系,以便在高考中取得好成绩。

机器学习的实现

机器学习的实现可以分为两步:训练和预测,类似于我们熟悉的归纳和演绎;

  • 归纳,即从具体案例中抽象一般规律,机器学习中的“训练”亦是如此(归纳是人通过案例,在大量的数据中去总结一般规律;而机器学习则是将数据“喂”给计算机,由计算机来总结规律)。
  • 演绎:从一般规律推导出具体案例的结果,机器学习中的“预测”亦是如此。基于训练得到的输出Y与输入X之间的关系,在输入原训练集中没有的数据X’时,输出的Y’与实际情况下的Y一致或者相近,那么我们认定模型是有效的。

机器学习的方法论

用一个例子“牛顿第二定律的推导”入手,介绍机器学习的思考过程,以及在过程中如何确定模型参数,模型三个关键部分(假设、评价、优化)该如何应用。

案例:牛顿第二定律

探索机器学习的方法论,和人类在早期推导数理知识时的过程有高度的相似性。下面以“机器从牛顿第二定律实验中学习知识”为例,帮助读者更加深入理解机器学习(监督学习)的方法论本质。

  • 牛顿第二定律
    牛顿第二定律是物理学家牛顿提出的,简单解释为:物体加速度的大小跟作用力成正比,跟物体的质量成反比,与物体质量的倒数成正比;用公式表示为:
    F = m a F = ma F=ma
    假设我们已经以质量为自变量,探究作用力X和加速度Y之间的关系,则得出的实验表格为:
    百度飞桨-手把手带你零基础实践深度学习-课程笔记(一)_第2张图片
    通过观察数据,我们做出猜测,物体的加速度a和作用力F之间的关系应该是线性关系。因此我们提出假设:
    a = w ⋅ F 。 其 中 , a 代 表 加 速 度 , F 代 表 作 用 力 , w 是 待 确 定 的 参 数 。 a=w⋅F。其中,a代表加速度,F代表作用力,w是待确定的参数。 a=wFaFw
    通过大量实验数据的训练,确定参数w是物体质量的倒数(1/m),即得到完整的模型公式
    a = F ⋅ ( 1 / m ) a=F⋅(1/m) a=F(1/m)
    当已知作用到某个物体的力时,基于模型可以快速预测物体的加速度。例如:燃料对火箭的推力F=10,火箭的质量m=2,可快速得出火箭的加速度a=5。

确定模型参数

这个有趣的案例演示了机器学习的基本过程,但其中有一个关键点的实现尚不清晰,即:如何确定模型参数(w=1/m)?
确定参数的过程与科学家提出假说的方式类似,合理的假说至少可以解释所有的已知观测数据。如果未来观测到不符合理论假说的新数据,科学家会尝试提出新的假说。(简单来说,先猜对,将得出的模型带入已知的数据验证,若与实际结果不符则根据差距再做出新的假设,不断逼近正确值直到猜中)
图3 是以H为模型的假设,它是一个关于参数W和输入X的函数,用H(W,X)表示。模型的优化目标是H(W,X)的输出与真实输出Y尽量一致,两者的相差程度即是模型效果的评价函数(相差越小越好)。那么,确定参数的过程就是在已知的样本上,不断减小该评价函数H(W,X) 和Y相差的过程,直到学习到一个参数W,使得评价函数的取值最小。这个衡量模型预测值和真实值差距的评价函数也被称为损失函数(损失Loss)。
百度飞桨-手把手带你零基础实践深度学习-课程笔记(一)_第3张图片
举例解释,模型就如同一个不会创新的学生,只能通过尝试答对(最小化损失)大量的习题(已知样本)来学习知识(模型参数W),并期望用学习到的知识(模型参数W),组成完整的模型H(W,X),回答不知道答案的考试题(未知样本)。
最小化损失是模型的优化目标,实现损失最小化的方法称为优化算法,也称为寻解算法(找到使得损失函数最小的参数解)。参数W和输入X组成公式的基本结构称为假设。在牛顿第二定律的案例中,基于对数据的观测,我们提出了线性假设,即作用力和加速度是线性关系,用线性方程表示。由此可见,模型假设、评价函数(损失/优化目标)和优化算法是构成模型的三个部分。

模型结构介绍

构成模型的三个部分(模型假设、评价函数和优化算法)是如何支撑机器学习流程的呢?
百度飞桨-手把手带你零基础实践深度学习-课程笔记(一)_第4张图片

  • 模型假设:世界上的可能关系千千万,漫无目标的试探Y ~ X之间的关系显然是十分低效的。因此假设空间先圈定了一个模型能够表达的可能关系(猜也不能随便猜,要大致符合数据变化规律,以减小工作量),如蓝色圆圈所示。机器还会进一步在假设圈定的圆圈内寻找最优的Y~X关系,即确定参数W。
  • 评价函数(损失函数):寻找最优之前,我们需要先定义什么是最优,即评价一个Y~X关系的好坏的指标。通常衡量该关系是否能很好的拟合现有观测样本,将拟合的误差最小作为优化目标。
  • 优化算法:设置了评价指标后,就可以在假设圈定的范围内,将使得评价指标最优(损失函数最小/最拟合已有观测样本)的Y~X关系找出来,这个寻找的方法即为优化算法。最笨的优化算法即按照参数的可能,穷举每一个可能取值来计算损失函数,保留使得损失函数最小的参数作为最终结果。
    从上述过程可以得出,机器学习的过程与牛顿第二定律的学习过程基本一致,都分为假设、评价和优化三个阶段:

1.假设:通过观察加速度a和作用力F的观测数据,假设aaa和FFF是线性关系,即a=w∗F
2.评价:对已知观测数据上的拟合效果好,即w∗F计算的结果,要和观测的a尽量接近。
3.优化:在参数w的所有可能取值中,发现w=1/m可使得评价最好(最拟合观测样本)。

机器执行学习的框架体现了其学习的本质是 “参数估计”(Learning is parameter estimation)。在此基础上,许多看起来完全不一样的问题都可以使用同样的框架进行学习,如科学定律、图像识别、机器翻译和自动问答等,它们的学习目标都是拟合一个“大公式”
百度飞桨-手把手带你零基础实践深度学习-课程笔记(一)_第5张图片

深度学习

机器学习算法理论在上个世纪90年代发展成熟,在许多领域都取得了成功应用。但平静的日子只延续到2010年左右,随着大数据的涌现和计算机算力提升,深度学习模型异军突起,极大改变了机器学习的应用格局。今天,多数机器学习任务都可以使用深度学习模型解决,尤其在在语音、计算机视觉和自然语言处理等领域,深度学习模型的效果比传统机器学习算法有显著提升。

那么相比传统的机器学习算法,深度学习做出了哪些改进呢?其实两者在理论结构上是一致的,即:模型假设、评价函数和优化算法,其根本差别在于假设的复杂度
百度飞桨-手把手带你零基础实践深度学习-课程笔记(一)_第6张图片
不是所有的任务都像牛顿第二定律那样简单直观。对于 图中的美女照片,人脑可以接收到五颜六色的光学信号,能用极快的速度反应出这张图片是一位美女,而且是程序员喜欢的类型。但对计算机而言,只能接收到一个数字矩阵,对于美女这种高级的语义概念,从像素到高级语义概念中间要经历的信息变换的复杂性是难以想象的!这种变换已经无法用数学公式表达,因此研究者们借鉴了人脑神经元的结构,设计出神经网络的模型。

神经网络的基本概念

人工神经网络包括多个神经网络层,如卷积层、全连接层、LSTM等,每一层又包括很多神经元,超过三层的非线性神经网络都可以被称为深度神经网络。通俗的讲,深度学习的模型可以视为是输入到输出的映射函数,如图像到高级语义(美女)的映射,足够深的神经网络理论上可以拟合任何复杂的函数。因此神经网络非常适合学习样本数据的内在规律和表示层次,对文字、图像和语音任务有很好的适用性。因为这几个领域的任务是人工智能的基础模块,所以深度学习被称为实现人工智能的基础也就不足为奇了。
百度飞桨-手把手带你零基础实践深度学习-课程笔记(一)_第7张图片

  • 神经元: 神经网络中每个节点称为神经元,由两部分组成:
  • 加权和:将所有输入加权求和
  • 非线性变换(激活函数):加权和的结果经过一个非线性函数变换,让神经元计算具备非线性的能力
  • 多层连接: 大量这样的节点按照不同的层次排布,形成多层的结构连接起来,即称为神经网络。
  • 前向计算: 从输入计算输出的过程,顺序从网络前至后
  • 计算图: 以图形化的方式展现神经网络的计算逻辑又称为计算图。我们也可以将神经网络的计算图以公式的方式表达如下:
    Y = f 3 ( f 2 ( f 1 ( w 1 ⋅ x 1 + w 2 ⋅ x 2 + w 3 + x 3 + b + . . . ) . . . ) . . . ) Y=f_3(f_2(f_1(w_1·x_1+w_2·x_2+w_3+x_3+b+...)...)...) Y=f3(f2(f1(w1x1+w2x2+w3+x3+b+...)...)...)
    由此可见,神经网络并没有那么神秘,它的本质是一个含有很多参数的“大公式”。

实践部分:波士顿房价预测

上一节我们初步认识了神经网络的基本概念(如神经元、多层连接、前向计算、计算图)和模型结构三要素(模型假设、评价函数和优化算法)。本节将以“波士顿房价”任务为例,向读者介绍使用Python语言和Numpy库来构建神经网络模型的思考过程和操作方法。

波士顿房价预测是一个经典的机器学习任务,类似于程序员世界的“Hello World”。和大家对房价的普遍认知相同,波士顿地区的房价是由诸多因素影响的。该数据集统计了13种可能影响房价的因素和该类型房屋的均价,期望构建一个基于13个因素进行房价预测的模型.(数据类型:一组数据包含13个特征,以及1个作为标签的房价,共14个元素)
百度飞桨-手把手带你零基础实践深度学习-课程笔记(一)_第8张图片
对于预测问题,可以根据预测输出的类型是连续的实数值,还是离散的标签,区分为回归任务和分类任务。因为房价是一个连续值,所以房价预测显然是一个回归任务。下面我们尝试用最简单的线性回归模型解决这个问题,并用神经网络来实现这个模型。

对于预测问题,可以根据预测输出的类型连续的实数值,还是离散的标签,区分为回归任务和分类任务。因为房价是一个连续值,所以房价预测显然是一个回归任务。下面我们尝试用最简单的线性回归模型解决这个问题,并用神经网络来实现这个模型。

线性回归模型

假设房价和各影响因素之间能够用线性关系来描述:
y = ∑ j = 1 M x j w j + b y=\sum^M_{j=1}x_jw_j+b y=j=1Mxjwj+b
其中未知的参数为wj和b,模型的求解则是通过数据拟合出每个wj和b。其中wj和b分别表示该线性模型的权重和偏置。以为情况下,wj和b是线性方程的斜率和截距。
线性回归模型使用均方误差作为损失函数(Loss),用以衡量预测房价和真实房价的差异,公式如下:
M S E = 1 n ∑ i = 1 n ( Y ^ i − Y I ) 2 MSE = \frac{1}{n}\sum^n_{i=1}(\hat Y_i-Y_I)^2 MSE=n1i=1n(Y^iYI)2
损失函数的设计不仅仅要考虑“合理性”,同样需要考虑“易解性”。 之所以使用均方差函数,一是因为采用均方差的Loss函数在每个点都可导;二是这样的Loss函数带有坡度,而坡度的变化为后面的参数优化提供了便利。

线性回归模型的神经网络结构

神经网络的标准结构中,每个神经元由加权和与非线性变换构成,然后将多个神经元分层的摆放并连接形成神经网络。
线性回归模型可以认为是神经网络模型的一种极简特例,是一个只有加权和、没有非线性变换的神经元(无需形成网络)。
百度飞桨-手把手带你零基础实践深度学习-课程笔记(一)_第9张图片

构建房价预测任务的神经网络模型(Python、Numpy)

深度学习不仅实现了模型的端到端学习,还推动了人工智能进入工业大生产阶段,产生了标准化、自动化和模块化的通用框架。不同场景的深度学习模型具备一定的通用性,五个步骤即可完成模型的构建和训练
百度飞桨-手把手带你零基础实践深度学习-课程笔记(一)_第10张图片

数据处理

数据处理包含五个部分:数据导入、数据形状变换、数据集划分、数据归一化处理和封装load data函数。数据预处理后,才能被模型调用。

读入数据:

通过以下代码读入数据,了解波士顿房价的数据结构;数据存放在本地目录下housing.data文件中

# 导入需要用到的package
import numpy as np
import json
# 读入训练数据
datafile = './housing.data'
data = np.fromfile(datafile, sep=' ')
print(data)
输出:
array([6.320e-03, 1.800e+01, 2.310e+00, ..., 3.969e+02, 7.880e+00,
       1.190e+01])


-----------------------------------------------------
#可以使用.shape方法来查看数据的长度
print(data.shape)
输出:
(7084, )
#这表明原始数据是一维,长度7084

数据形状变换

由于读入的原始数据是一维的,所有数据都连在一起。因此需要我们将数据的形状进行变换,形成一个2维的矩阵,每行为一个数据样本(14个值),每个数据样本包含13个X(影响房价的特征)和一个Y(该类型房屋的均价)

# 读入之后的数据被转化成1维array,其中array的第0-13项是第一条数据,第14-27项是第二条数据,以此类推.... 
# 这里对原始数据做reshape,变成N x 14的形式
feature_names = [ 'CRIM', 'ZN', 'INDUS', 'CHAS', 'NOX', 'RM', 'AGE','DIS', 
                 'RAD', 'TAX', 'PTRATIO', 'B', 'LSTAT', 'MEDV' ]
feature_num = len(feature_names)

#reshape方法:(行数, 列数) <--转换后
#//代表整数除法,结果为int类型数值
data = data.reshape([data.shape[0] // feature_num, feature_num])

# 查看数据
#取data中第一行数据,长度为14,符合预期目标
x = data[0]
print(x.shape)
print(x)
输出:
(14,)
[6.320e-03 1.800e+01 2.310e+00 0.000e+00 5.380e-01 6.575e+00 6.520e+01
 4.090e+00 1.000e+00 2.960e+02 1.530e+01 3.969e+02 4.980e+00 2.400e+01]

数据集划分

将数据集划分成训练集和测试集,其中训练集用于确定模型的参数,测试集用于评判模型的效果。为什么要对数据集进行拆分,而不能直接应用于模型训练呢?道理与学生时代的授课和考试关系比较类似
百度飞桨-手把手带你零基础实践深度学习-课程笔记(一)_第11张图片
学校讲解题目,是希望学生去学习、理解和总结题目背后的知识规律,而不是习题本身;当考试的题目与平时讲解不一致时(只是改变表达方法,不改变考点),才能考察学生的学习情况。
同理,在机器学习中,想要检验模型的准确性,就需要用它没见过的独立同分布数据来检验一下,所以我们将数据集拆分成训练集和测试集两个部分来使用。
在本案例中,我们将80%的数据用作训练集,20%用作测试集,实现代码如下。
通过打印训练集的形状,可以发现共有404个样本,每个样本含有13个特征和1个预测值。

ratio = 0.8
offset = int(data.shape[0] * ratio)
training_data = data[:offset]
training_data.shape
--------------------------------------------
(404, 14)

数据归一化

对每个特征进行归一化处理,将特征值转化为每个数据的幅度,使得每个特征的取值缩放到0~1之间。
这样做有两个好处:一是模型训练更高效(减小计算量);二是特征前的权重大小可以代表该变量对预测结果的贡献度(因为每个特征值本身的范围相同)

# 计算train数据集的最大值,最小值,平均值
maximums, minimums, avgs = \
                     training_data.max(axis=0), \
                     training_data.min(axis=0), \
     training_data.sum(axis=0) / training_data.shape[0]
# 对数据进行归一化处理
for i in range(feature_num):
    #print(maximums[i], minimums[i], avgs[i])
    data[:, i] = (data[:, i] - avgs[i]) / (maximums[i] - minimums[i])

封装成load_data函数

将上述几个数据处理操作封装成load data函数,以便下一步模型的调用

def load_data():
    # 从文件导入数据
    datafile = './housing.data'
    data = np.fromfile(datafile, sep=' ')

    # 每条数据包括14项,其中前面13项是影响因素,第14项是相应的房屋价格中位数
    feature_names = [ 'CRIM', 'ZN', 'INDUS', 'CHAS', 'NOX', 'RM', 'AGE', \
                      'DIS', 'RAD', 'TAX', 'PTRATIO', 'B', 'LSTAT', 'MEDV' ]
    feature_num = len(feature_names)

    # 将原始数据进行Reshape,变成[N, 14]这样的形状
    data = data.reshape([data.shape[0] // feature_num, feature_num])

    # 将原数据集拆分成训练集和测试集
    # 这里使用80%的数据做训练,20%的数据做测试
    # 测试集和训练集必须是没有交集的
    ratio = 0.8
    offset = int(data.shape[0] * ratio)
    training_data = data[:offset]

    # 计算训练集的最大值,最小值,平均值
    maximums, minimums, avgs = training_data.max(axis=0), training_data.min(axis=0), \
                                 training_data.sum(axis=0) / training_data.shape[0]

    # 对数据进行归一化处理
    for i in range(feature_num):
        #print(maximums[i], minimums[i], avgs[i])
        data[:, i] = (data[:, i] - avgs[i]) / (maximums[i] - minimums[i])

    # 训练集和测试集的划分比例
    training_data = data[:offset]
    test_data = data[offset:]
    return training_data, test_data

模型设计

模型设计是深度学习模型关键要素之一,也称为网络结构设计,相当于模型的假设空间,即实现模型“前向计算”(从输入到输出)的过程。

如果将输入特征和输出预测值均以向量表示,输入特征x有13个分量,y有1个分量,那么参数权重的形状(shape)是13×1。假设我们以如下任意数字赋值参数做初始化:
w = [ 0.1 , 0.2 , 0.3 , 0.4 , 0.5 , 0.6 , 0.7 , 0.8 , − 0.1 , − 0.2 , − 0.3 , − 0.4 , 0.0 ] w=[0.1,0.2,0.3,0.4,0.5,0.6,0.7,0.8,-0.1,-0.2,-0.3,-0.4,0.0] w=[0.1,0.2,0.3,0.4,0.5,0.6,0.7,0.8,0.1,0.2,0.3,0.4,0.0]

w=[0.1,0.2,0.3,0.4,0.5,0.6,0.7,0.8,-0.1,-0.2,-0.3,-0.4,0.0]
w.np.array(w).reshape([13, 1])

取出第1条样本数据,观察样本的特征向量与参数向量相乘的结果。

x1=x[0]
t = np.dot(x1, w)
print(t)
输出:
[0.03395597]

完整的线性回归公式,还需要初始化偏移量bbb,同样随意赋初值b=-0.2。那么,线性回归模型的完整输出是z=t+b,这个从特征和参数计算输出值的过程称为“前向计算”。

b = -0.2
z = t + b
print(z)
输出:
[-0.16604403]

将上述计算预测输出的过程以“类和对象”的方式来描述,类成员变量有参数www和bbb。通过写一个forward函数(代表“前向计算”)完成上述从特征和参数到输出预测值的计算过程

class Network(object):
    def __init__(self, num_of_weights):
        # 随机产生w的初始值
        # 为了保持程序每次运行结果的一致性,
        # 此处设置固定的随机数种子
        np.random.seed(0)
        self.w = np.random.randn(num_of_weights, 1)
        self.b = 0.
        
    def forward(self, x):
        z = np.dot(x, self.w) + self.b
        return z

基于Network类的定义,模型的计算过程如下所示

net = Network(13)
x1 = x[0]
y1 = y[0]
z = net.forward(x1)
print(z)
输出:
[-0.63182506]

训练配置

模型设计完成后,需要通过训练配置寻找模型的最优值,即通过损失函数来衡量模型的好坏。训练配置也是深度学习模型关键要素之一。

通过模型计算x1表示的影响因素所对应的房价应该是z, 但实际数据告诉我们房价是y。这时我们需要有某种指标来衡量预测值z跟真实值y之间的差距。对于回归问题,最常采用的衡量方法是使用均方误差作为评价模型好坏的指标,具体定义如下:
L o s s = ( y − z ) 2 Loss = (y-z)^2 Loss=(yz)2
上式中的Loss(简记为: L)通常也被称作损失函数,它是衡量模型好坏的指标。在回归问题中均方误差是一种比较常见的形式,分类问题中通常会采用交叉熵作为损失函数,在后续的章节中会更详细的介绍。对一个样本计算损失函数值的实现如下:

Loss = (y1 - z)*(y1 - z)
print(Loss)
输出:
[0.39428312]

因为计算损失函数时需要把每个样本的损失函数值都考虑到,所以我们需要对单个样本的损失函数进行求和,并除以样本总数N
L o s s = 1 N ∑ i = 1 N ( y i − z i ) 2 Loss=\frac{1}{N}\sum^N_{i=1}(y_i-z_i)^2 Loss=N1i=1N(yizi)2
在Network类下面添加损失函数的计算过程如下

class Network(object):
    def __init__(self, num_of_weights):
        # 随机产生w的初始值
        # 为了保持程序每次运行结果的一致性,此处设置固定的随机数种子
        np.random.seed(0)
        self.w = np.random.randn(num_of_weights, 1)
        self.b = 0.
        
    def forward(self, x):
        z = np.dot(x, self.w) + self.b
        return z
    
    def loss(self, z, y):
        error = z - y
        cost = error * error
        cost = np.mean(cost)
        return cost

使用定义的Network类,可以方便的计算预测值和损失函数。需要注意的是,类中的变量x, w,b, z, error等均是向量。
以变量x为例,共有两个维度,一个代表特征数量(值为13),一个代表样本数量,代码如下所示

net = Network(13)
# 此处可以一次性计算多个样本的预测值和损失函数
#Numpy的广播功能:便捷的实现多样本的计算(广播功能:像使用单一变量一样操作数组)
x1 = x[0:3]
y1 = y[0:3]
z = net.forward(x1)
print('predict: ', z)
loss = net.loss(z, y1)
print('loss:', loss)
输出:
predict:  [[-0.63182506]
 [-0.55793096]
 [-1.00062009]]
loss: 0.7229825055441156

训练过程

上述计算过程描述了如何构建神经网络,通过神经网络完成预测值和损失函数的计算。接下来介绍如何求解参数w和b的数值,这个过程也称为模型训练过程。
训练过程是深度学习模型的关键要素之一,其目标是让定义的损失函数Loss尽可能的小,也就是说找到一个参数解w和b,使得损失函数取得极小值。
我们先做一个小测试:如 图所示,基于微积分知识,求一条曲线在某个点的斜率等于函数该点的导数值。那么大家思考下,当处于曲线的极值点时,该点的斜率是多少?
百度飞桨-手把手带你零基础实践深度学习-课程笔记(一)_第12张图片
这个问题并不难回答,处于曲线极值点时的斜率为0,即函数在极值点处的导数为0。那么,让损失函数取极小值的w和b应该是下述方程组的解
∂ L ∂ w = 0 , ∂ L ∂ b = 0 \frac{∂L}{∂w}=0 , \frac{∂L}{∂b}=0 wL=0bL=0
将样本数据(x,y)带入上面的方程组中即可求解出w和b的值,但是这种方法只对线性回归这样简单的任务有效。如果模型中含有非线性变换,或者损失函数不是均方差这种简单的形式,则很难通过上式求解。
在现实问题中,很多损失函数的导函数很多都是一些不可逆的函数,或者多数都有不可逆的倾向不可逆:已知函数的x,很容易求解y;而已知y却很难或无法求解x)。
为了解决这个问题,下面我们将引入更加普适的数值求解方法:梯度下降法。

梯度下降法

在现实中存在大量的函数正向求解容易,反向求解较难,被称为单向函数。这种函数在密码学中有大量的应用,密码锁的特点是可以迅速判断一个密钥是否是正确的(已知x,求y很容易),但是即使获取到密码锁系统,无法破解出正确的密钥是什么(已知y,求x很难)。

这种情况特别类似于一位想从山峰走到坡谷的盲人,他看不见坡谷在哪(无法逆向求解出Loss导数为0时的参数值),但可以伸脚探索身边的坡度(当前点的导数值,也称为梯度)。那么,求解Loss函数最小值可以这样实现:从当前的参数取值,一步步的按照下坡的方向下降,直到走到最低点。这种方法笔者称它为“盲人下坡法”。哦不,有个更正式的说法“梯度下降法”。

训练的关键是找到一组(w,b),使得损失函数L取极小值。我们先看一下损失函数LLL只随两个参数w5、w9变化时的简单情形,启发下寻解的思路。
L = L ( w 5 , w 9 ) L=L(w_5,w_9) L=L(w5,w9)
这里我们将w0,w1,…,w12中除w5,w9之外的参数和b都固定下来,可以用图画出L(w5,w9)的形式。

net = Network(13)
losses = []
#只画出参数w5和w9在区间[-160, 160]的曲线部分,以及包含损失函数的极值
w5 = np.arange(-160.0, 160.0, 1.0)
w9 = np.arange(-160.0, 160.0, 1.0)
losses = np.zeros([len(w5), len(w9)])

#计算设定区域内每个参数取值所对应的Loss
for i in range(len(w5)):
    for j in range(len(w9)):
        net.w[5] = w5[i]
        net.w[9] = w9[j]
        z = net.forward(x)
        loss = net.loss(z, y)
        losses[i, j] = loss

#使用matplotlib将两个变量和对应的Loss作3D图
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import Axes3D
fig = plt.figure()
ax = Axes3D(fig)

w5, w9 = np.meshgrid(w5, w9)

ax.plot_surface(w5, w9, losses, rstride=1, cstride=1, cmap='rainbow')
plt.show()

百度飞桨-手把手带你零基础实践深度学习-课程笔记(一)_第13张图片
对于这种简单情形,我们利用上面的程序,可以在三维空间中画出损失函数随参数变化的曲面图。从图中可以看出有些区域的函数值明显比周围的点小。
需要说明的是:为什么这里我们选择w5和w9来画图?这是因为选择这两个参数的时候,可比较直观的从损失函数的曲面图上发现极值点的存在。其他参数组合,从图形上观测损失函数的极值点不够直观。

观察上述曲线呈现出“圆滑”的坡度,这正是我们选择以均方误差作为损失函数的原因之一。图6 呈现了只有一个参数维度时,均方误差和绝对值误差(只将每个样本的误差累加,不做平方处理)的损失函数曲线图。
百度飞桨-手把手带你零基础实践深度学习-课程笔记(一)_第14张图片
由此可见,均方误差表现的“圆滑”的坡度有两个好处:

  • 曲线的最低点是可导的。
  • 越接近最低点,曲线的坡度逐渐放缓,有助于通过当前的梯度来判断接近最低点的程度(是否逐渐减少步长,以免错过最低点)。
    而这两个特性绝对值误差是不具备的,这也是损失函数的设计不仅仅要考虑“合理性”,还要追求“易解性”的原因。

现在我们要找出一组[w5,w9]的值,使得损失函数最小,实现梯度下降法的方案如下:

  • 步骤1:随机的选一组初始值,例如:[w5,w9]=[−100.0,−100.0]
  • 步骤2:选取下一个点[w5′,w9′],使得L(w5′,w9′)
  • 步骤3:重复步骤2,直到损失函数几乎不再下降。

如何选择[w5′,w9′]是至关重要的,第一要保证L是下降的,第二要使得下降的趋势尽可能的快。微积分的基础知识告诉我们,沿着梯度的反方向,是函数值下降最快的方向,如 图所示。简单理解,函数在某一个点的梯度方向是曲线斜率最大的方向,但梯度方向是向上的,所以下降最快的是梯度的反方向。
百度飞桨-手把手带你零基础实践深度学习-课程笔记(一)_第15张图片

计算梯度

上面(训练配置处)我们讲过了损失函数的计算方法,这里稍微加以改写。为了梯度计算更加简洁,引入因子1/2,定义损失函数如下:
L = 1 2 N ∑ i = 1 N ( y i − z i ) 2 L=\frac{1}{2}N\sum^N_{i=1}(y_i-z_i)^2 L=21Ni=1N(yizi)2
其中zi是网络对第i个样本的预测值:
z i = ∑ j = 0 12 x i j w j + b z_i=\sum^{12}_{j=0}x_i^jw_j+b zi=j=012xijwj+b
梯度的定义:
g r a d i e n t = ( ∂ L ∂ w 0 , ∂ L ∂ w 1 , . . . , ∂ L ∂ w 1 2 , ∂ L ∂ b ) gradient=(\frac{∂L}{∂w_0},\frac{∂L}{∂w_1},...,\frac{∂L}{∂w_12},\frac{∂L}{∂b}) gradient=(w0L,w1L,...,w12L,bL)
可以计算出L对w和b的偏导数:
∂ L ∂ w j = 1 N ∑ i = 1 N ( z i − y i ) ∂ z i ∂ w j = 1 N ∑ i = 1 N ( z i − y i ) x i J , ∂ L ∂ b = 1 N ∑ i = 1 N ( z i − y i ) ∂ z i ∂ b = 1 N ∑ i = 1 N ( z i − y i ) \frac{∂L}{∂w_j}=\frac{1}{N}\sum^N_{i=1}(z_i-y_i)\frac{∂z_i}{∂w_j}=\frac{1}{N}\sum^N_{i=1}(z_i-y_i)x^J_i, \\ \frac{∂L}{∂b}=\frac{1}{N}\sum^N_{i=1}(z_i-y_i)\frac{∂z_i}{∂b}=\frac{1}{N}\sum^N_{i=1}(z_i-y_i) wjL=N1i=1N(ziyi)wjzi=N1i=1N(ziyi)xiJ,bL=N1i=1N(ziyi)bzi=N1i=1N(ziyi)
从导数的计算过程可以看出,因子1/2被消掉了,这是因为二次函数求导的时候会产生因子2,这也是我们将损失函数改写的原因。
考虑只有一个样本的情况:
L = 1 2 ( y 1 − z 1 ) 2 z 1 = x 1 0 ⋅ w 0 + x 1 1 ⋅ w 1 + . . . + x 1 1 2 ⋅ w 12 + b L=\frac{1}{2}(y_1-z_1)^2 \\ z_1=x_1^0·w_0+x^1_1·w_1+...+x_1^12·w_{12}+b L=21(y1z1)2z1=x10w0+x11w1+...+x112w12+b
计算得:
L = 1 2 ( x 1 0 ⋅ w 0 + x 1 1 ⋅ w 1 + . . . + x 1 1 2 ⋅ w 12 + b − y 1 ) 2 L=\frac{1}{2}(x_1^0·w_0+x^1_1·w_1+...+x_1^12·w_{12}+b-y_1)^2 L=21(x10w0+x11w1+...+x112w12+by1)2
偏导数:
∂ L ∂ w 0 = ( x 1 0 ⋅ w 0 + x 1 1 ⋅ w 1 + . . . + x 1 1 2 ⋅ w 12 + b − y 1 ) ⋅ x 1 0 = ( z 1 − y 1 ) ⋅ x 1 0 ∂ L ∂ b = ( x 1 0 ⋅ w 0 + x 1 1 ⋅ w 1 + . . . + x 1 1 2 ⋅ w 12 + b − y 1 ) ⋅ 1 = ( z 1 − y 1 ) \frac{∂L}{∂w_0}=(x_1^0·w_0+x^1_1·w_1+...+x_1^12·w_{12}+b-y_1)·x_1^0 \\=(z_1-y_1)·x_1^0 \\ \frac{∂L}{∂b}=(x_1^0·w_0+x^1_1·w_1+...+x_1^12·w_{12}+b-y_1)·1 \\= (z_1-y_1) w0L=(x10w0+x11w1+...+x112w12+by1)x10=(z1y1)x10bL=(x10w0+x11w1+...+x112w12+by1)1=(z1y1)
可以通过具体的程序查看每个变量的数据和维度

x1 = x[0]
y1 = y[0]
z1 = net.forward(x1)
print('x1 {}, shape {}'.format(x1, x1.shape))
print('y1 {}, shape {}'.format(y1, y1.shape))
print('z1 {}, shape {}'.format(z1, z1.shape))
输出:
x1 [-0.02146321  0.03767327 -0.28552309 -0.08663366  0.01289726  0.04634817
  0.00795597 -0.00765794 -0.25172191 -0.11881188 -0.29002528  0.0519112
 -0.17590923], shape (13,)
y1 [-0.00390539], shape (1,)
z1 [-12.05947643], shape (1,)

按上面的公式,当只有一个样本时,可以计算某个wj ,比如w0的梯度

gradient_w0 = (z1 - y1) * x1[0]
print('gradient_w0 {}'.format(gradient_w0))
输出:
gradient_w0 [0.25875126]

同样我们可以计算w1的梯度。

gradient_w1 = (z1 - y1) * x1[1]
print('gradient_w1 {}'.format(gradient_w1))
输出:
gradient_w1 [-0.45417275]

依次计算w2的梯度

gradient_w2= (z1 - y1) * x1[2]
print('gradient_w1 {}'.format(gradient_w2))
输出:
gradient_w1 [3.44214394]

当然,也写一个for循环即可计算从w0到w12 的所有权重的梯度。
将上面计算w和b的梯度的过程,写成Network类的gradient函数,实现方法如下所示

class Network(object):
    def __init__(self, num_of_weights):
        # 随机产生w的初始值
        # 为了保持程序每次运行结果的一致性,此处设置固定的随机数种子
        np.random.seed(0)
        self.w = np.random.randn(num_of_weights, 1)
        self.b = 0.
        
    def forward(self, x):
        z = np.dot(x, self.w) + self.b
        return z
    
    def loss(self, z, y):
        error = z - y
        num_samples = error.shape[0]
        cost = error * error
        cost = np.sum(cost) / num_samples
        return cost
    
    def gradient(self, x, y):
        z = self.forward(x)
        gradient_w = (z-y)*x
        gradient_w = np.mean(gradient_w, axis=0)
        gradient_w = gradient_w[:, np.newaxis]
        gradient_b = (z - y)
        gradient_b = np.mean(gradient_b)
        
        return gradient_w, gradient_b
# 调用上面定义的gradient函数,计算梯度
# 初始化网络
net = Network(13)
# 设置[w5, w9] = [-100., -100.]
net.w[5] = -100.0
net.w[9] = -100.0

z = net.forward(x)
loss = net.loss(z, y)
gradient_w, gradient_b = net.gradient(x, y)
gradient_w5 = gradient_w[5][0]
gradient_w9 = gradient_w[9][0]
print('point {}, loss {}'.format([net.w[5][0], net.w[9][0]], loss))
print('gradient {}'.format([gradient_w5, gradient_w9]))
输出:
point [-100.0, -100.0], loss 686.3005008179159
gradient [-0.850073323995813, -6.138412364807849]

确定损失函数更小的点

下面我们开始研究更新梯度的方法。首先沿着梯度的反方向移动一小步,找到下一个点P1,观察损失函数的变化

# 在[w5, w9]平面上,沿着梯度的反方向移动到下一个点P1
# 定义移动步长 eta
eta = 0.1
# 更新参数w5和w9
net.w[5] = net.w[5] - eta * gradient_w5
net.w[9] = net.w[9] - eta * gradient_w9
# 重新计算z和loss
z = net.forward(x)
loss = net.loss(z, y)
gradient_w, gradient_b = net.gradient(x, y)
gradient_w5 = gradient_w[5][0]
gradient_w9 = gradient_w[9][0]
输出:
print('point {}, loss {}'.format([net.w[5][0], net.w[9][0]], loss))
print('gradient {}'.format([gradient_w5, gradient_w9]))
point [-99.91499266760042, -99.38615876351922], loss 678.6472185028845
gradient [-0.8556356178645292, -6.0932268634065805]

运行上面的代码,可以发现沿着梯度反方向走一小步,下一个点的损失函数的确减少了
在上述代码中,每次更新参数使用的语句:
net.w[5] = net.w[5] - eta * gradient_w5

  • 相减:参数需要向梯度的反方向移动。
  • eta:控制每次参数值沿着梯度反方向变动的大小,即每次移动的步长,又称为学习率。
    为什么之前我们要做输入特征的归一化,保持尺度一致?这是为了让统一的步长更加合适。特征输入归一化后,不同参数输出的Loss是一个比较规整的曲线,学习率可以设置成统一的值 ;特征输入未归一化时,不同特征对应的参数所需的步长不一致,尺度较大的参数需要大步长,尺寸较小的参数需要小步长,导致无法设置统一的学习率。
    百度飞桨-手把手带你零基础实践深度学习-课程笔记(一)_第16张图片
    将上面的循环计算过程封装在train和update函数中
class Network(object):
    def __init__(self, num_of_weights):
        # 随机产生w的初始值
        # 为了保持程序每次运行结果的一致性,此处设置固定的随机数种子
        np.random.seed(0)
        self.w = np.random.randn(num_of_weights,1)
        self.w[5] = -100.
        self.w[9] = -100.
        self.b = 0.
        
    def forward(self, x):
        z = np.dot(x, self.w) + self.b
        return z
    
    def loss(self, z, y):
        error = z - y
        num_samples = error.shape[0]
        cost = error * error
        cost = np.sum(cost) / num_samples
        return cost
    
    def gradient(self, x, y):
        z = self.forward(x)
        gradient_w = (z-y)*x
        gradient_w = np.mean(gradient_w, axis=0)
        gradient_w = gradient_w[:, np.newaxis]
        gradient_b = (z - y)
        gradient_b = np.mean(gradient_b)        
        return gradient_w, gradient_b
    
    def update(self, graident_w5, gradient_w9, eta=0.01):
        net.w[5] = net.w[5] - eta * gradient_w5
        net.w[9] = net.w[9] - eta * gradient_w9
        
    def train(self, x, y, iterations=100, eta=0.01):
        points = []
        losses = []
        for i in range(iterations):
            points.append([net.w[5][0], net.w[9][0]])
            z = self.forward(x)
            L = self.loss(z, y)
            gradient_w, gradient_b = self.gradient(x, y)
            gradient_w5 = gradient_w[5][0]
            gradient_w9 = gradient_w[9][0]
            self.update(gradient_w5, gradient_w9, eta)
            losses.append(L)
            if i % 50 == 0:
                print('iter {}, point {}, loss {}'.format(i, [net.w[5][0], net.w[9][0]], L))
        return points, losses
# 获取数据
train_data, test_data = load_data()
x = train_data[:, :-1]
y = train_data[:, -1:]
# 创建网络
net = Network(13)
num_iterations=2000
# 启动训练
points, losses = net.train(x, y, iterations=num_iterations, eta=0.01)

# 画出损失函数的变化趋势
plot_x = np.arange(num_iterations)
plot_y = np.array(losses)
plt.plot(plot_x, plot_y)
plt.show()
输出:
iter 0, point [-99.99144364382136, -99.93861587635192], loss 686.3005008179159
iter 50, point [-99.56362583488914, -96.92631128470325], loss 649.221346830939
iter 100, point [-99.13580802595692, -94.02279509580971], loss 614.6970095624063
iter 150, point [-98.7079902170247, -91.22404911807594], loss 582.543755023494
iter 200, point [-98.28017240809248, -88.52620357520894], loss 552.5911329872217
iter 250, point [-97.85235459916026, -85.9255316243737], loss 524.6810152322887
iter 300, point [-97.42453679022805, -83.41844407682491], loss 498.6667034691001
iter 350, point [-96.99671898129583, -81.00148431353688], loss 474.4121018974464
iter 400, point [-96.56890117236361, -78.67132338862874], loss 451.7909497114133
iter 450, point [-96.14108336343139, -76.42475531364933], loss 430.68610920670284
iter 500, point [-95.71326555449917, -74.25869251604028], loss 410.988905460488
iter 550, point [-95.28544774556696, -72.17016146534513], loss 392.5985138460824
iter 600, point [-94.85762993663474, -70.15629846096763], loss 375.4213919156372
iter 650, point [-94.42981212770252, -68.21434557551346], loss 359.3707524354014
iter 700, point [-94.0019943187703, -66.34164674796719], loss 344.36607459115214
iter 750, point [-93.57417650983808, -64.53564402117185], loss 330.33265059761464
iter 800, point [-93.14635870090586, -62.793873918279786], loss 317.2011651461846
iter 850, point [-92.71854089197365, -61.11396395304264], loss 304.907305311265
iter 900, point [-92.29072308304143, -59.49362926899678], loss 293.3913987080144
iter 950, point [-91.86290527410921, -57.930669402782904], loss 282.5980778542974
iter 1000, point [-91.43508746517699, -56.4229651670156], loss 272.47596883802515
iter 1050, point [-91.00726965624477, -54.968475648286564], loss 262.9774025287022
iter 1100, point [-90.57945184731255, -53.56523531604897], loss 254.05814669965383
iter 1150, point [-90.15163403838034, -52.21135123828792], loss 245.67715754581488
iter 1200, point [-89.72381622944812, -50.90500040003218], loss 237.796349191773
iter 1250, point [-89.2959984205159, -49.6444271209092], loss 230.3803798866218
iter 1300, point [-88.86818061158368, -48.42794056808474], loss 223.3964536766492
iter 1350, point [-88.44036280265146, -47.2539123610643], loss 216.81413643451378
iter 1400, point [-88.01254499371925, -46.12077426496303], loss 210.60518520483126
iter 1450, point [-87.58472718478703, -45.027015968976976], loss 204.74338990147896
iter 1500, point [-87.15690937585481, -43.9711829469081], loss 199.20442646183588
iter 1550, point [-86.72909156692259, -42.95187439671279], loss 193.96572062803054
iter 1600, point [-86.30127375799037, -41.96774125615467], loss 189.00632158541163
iter 1650, point [-85.87345594905815, -41.017484291751295], loss 184.3067847442463
iter 1700, point [-85.44563814012594, -40.0998522583068], loss 179.84906300239203
iter 1750, point [-85.01782033119372, -39.21364012642417], loss 175.61640587468244
iter 1800, point [-84.5900025222615, -38.35768737548557], loss 171.59326591927967
iter 1850, point [-84.16218471332928, -37.530876349682856], loss 167.76521193253296
iter 1900, point [-83.73436690439706, -36.73213067476985], loss 164.11884842217898
iter 1950, point [-83.30654909546485, -35.96041373329276], loss 160.64174090423475

百度飞桨-手把手带你零基础实践深度学习-课程笔记(一)_第17张图片

训练扩展到全部参数

房价预测的完整模型,必须要对所有参数www和bbb进行求解。这需要将Network中的update和train函数进行修改。由于不再限定参与计算的参数(所有参数均参与计算),修改之后的代码反而更加简洁。实现逻辑:“前向计算输出、根据输出和真实值计算Loss、基于Loss和输入计算梯度、根据梯度更新参数值”四个部分反复执行,直到到达参数最优点。

class Network(object):
    def __init__(self, num_of_weights):
        # 随机产生w的初始值
        # 为了保持程序每次运行结果的一致性,此处设置固定的随机数种子
        np.random.seed(0)
        self.w = np.random.randn(num_of_weights, 1)
        self.b = 0.
        
    def forward(self, x):
        z = np.dot(x, self.w) + self.b
        return z
    
    def loss(self, z, y):
        error = z - y
        num_samples = error.shape[0]
        cost = error * error
        cost = np.sum(cost) / num_samples
        return cost
    
    def gradient(self, x, y):
        z = self.forward(x)
        gradient_w = (z-y)*x
        gradient_w = np.mean(gradient_w, axis=0)
        gradient_w = gradient_w[:, np.newaxis]
        gradient_b = (z - y)
        gradient_b = np.mean(gradient_b)        
        return gradient_w, gradient_b
    
    def update(self, gradient_w, gradient_b, eta = 0.01):
        self.w = self.w - eta * gradient_w
        self.b = self.b - eta * gradient_b
        
    def train(self, x, y, iterations=100, eta=0.01):
        losses = []
        for i in range(iterations):
            z = self.forward(x)
            L = self.loss(z, y)
            gradient_w, gradient_b = self.gradient(x, y)
            self.update(gradient_w, gradient_b, eta)
            losses.append(L)
            if (i+1) % 10 == 0:
                print('iter {}, loss {}'.format(i, L))
        return losses

随机梯度下降法( Stochastic Gradient Descent)

在上述程序中,每次损失函数和梯度计算都是基于数据集中的全量数据。对于波士顿房价预测任务数据集而言,样本数比较少,只有404个。但在实际问题中,数据集往往非常大,如果每次都使用全量数据进行计算,效率非常低,通俗地说就是“杀鸡焉用牛刀”。
由于参数每次只沿着梯度反方向更新一点点,因此方向并不需要那么精确。一个合理的解决方案是每次从总的数据集中随机抽取出小部分数据来代表整体,基于这部分数据计算梯度和损失来更新参数,这种方法被称作随机梯度下降法(Stochastic Gradient Descent,SGD),核心概念如下:

  • mini-batch:每次迭代时抽取出来的一批数据被称为一个mini-batch。
  • batch_size:一个mini-batch所包含的样本数目称为batch_size。
  • epoch:当程序迭代的时候,按mini-batch逐渐抽取出样本,当把整个数据集都遍历到了的时候,则完成了一轮训练,也叫一个epoch。启动训练时,可以将训练的轮数num_epochs和batch_size作为参数传入。

下面结合程序介绍具体的实现过程,涉及到数据处理和训练过程两部分代码的修改。

数据处理代码修改

数据处理需要实现拆分数据批次和样本乱序(为了实现随机抽样的效果)两个功能。

# 获取数据
train_data, test_data = load_data()
train_data.shape
输出:
(404, 14)

train_data中一共包含404条数据,如果batch_size=10,即取前0-9号样本作为第一个mini-batch,命名train_data1。

train_data1 = train_data[0:10]
train_data1.shape
输出:
(10, 14)

使用train_data1的数据(0-9号样本)计算梯度并更新网络参数

在这里插入代码片net = Network(13)
x = train_data1[:, :-1]
y = train_data1[:, -1:]
loss = net.train(x, y, iterations=1, eta=0.01)
loss
输出:
[0.9001866101467375]

再取出10-19号样本作为第二个mini-batch,计算梯度并更新网络参数

train_data2 = train_data[10:19]
x = train_data1[:, :-1]
y = train_data1[:, -1:]
loss = net.train(x, y, iterations=1, eta=0.01)
loss
输出:
[0.8903272433979657]

按此方法不断的取出新的mini-batch,并逐渐更新网络参数。

接下来,将train_data分成大小为batch_size的多个mini_batch,如下代码所示:将train_data分成 404/10 + 1 = 41 个 mini_batch了,其中前40个mini_batch,每个均含有10个样本,最后一个mini_batch只含有4个样本。

batch_size = 10
n = len(train_data)
mini_batches = [train_data[k:k+batch_size] for k in range(0, n, batch_size)]
print('total number of mini_batches is ', len(mini_batches))
print('first mini_batch shape ', mini_batches[0].shape)
print('last mini_batch shape ', mini_batches[-1].shape)
total number of mini_batches is  41
first mini_batch shape  (10, 14)
last mini_batch shape  (4, 14)

另外,我们这里是按顺序取出mini_batch的,而SGD里面是随机抽取一部分样本代表总体。为了实现随机抽样的效果,我们先将train_data里面的样本顺序随机打乱,然后再抽取mini_batch。随机打乱样本顺序,需要用到np.random.shuffle函数,下面先介绍它的用法

a = np.array([1,2,3,4,5,6,7,8,9,10,11,12])
a = a.reshape([6, 2])
print('before shuffle\n', a)
np.random.shuffle(a)
print('after shuffle\n', a)
输出:
before shuffle
 [[ 1  2]
 [ 3  4]
 [ 5  6]
 [ 7  8]
 [ 9 10]
 [11 12]]
after shuffle
 [[ 1  2]
 [ 3  4]
 [ 5  6]
 [ 9 10]
 [11 12]
 [ 7  8]]

观察运行结果可发现,数组的元素在第0维被随机打乱,但第1维的顺序保持不变。例如数字2仍然紧挨在数字1的后面,数字8仍然紧挨在数字7的后面,而第二维的[3, 4]并不排在[1, 2]的后面。将这部分实现SGD算法的代码集成到Network类中的train函数中,最终的完整代码如下

# 获取数据
train_data, test_data = load_data()

# 打乱样本顺序
np.random.shuffle(train_data)

# 将train_data分成多个mini_batch
batch_size = 10
n = len(train_data)
mini_batches = [train_data[k:k+batch_size] for k in range(0, n, batch_size)]

# 创建网络
net = Network(13)

# 依次使用每个mini_batch的数据
for mini_batch in mini_batches:
    x = mini_batch[:, :-1]
    y = mini_batch[:, -1:]
    loss = net.train(x, y, iterations=1)

训练过程代码修改

将每个随机抽取的mini-batch数据输入到模型中用于参数训练。训练过程的核心是两层循环:

  • 第一层循环,代表样本集合要被训练遍历几次,称为“epoch”,代码如下:
for epoch_id in range(num_epoches):
  • 第二层循环,代表每次遍历时,样本集合被拆分成的多个批次,需要全部执行训练,称为“iter (iteration)”,代码如下:
for iter_id,mini_batch in emumerate(mini_batches):

在两层循环的内部是经典的四步训练流程:
前向计算->计算损失->计算梯度->更新参数,这与之前所学是一致的,代码如下:

x = mini_batch[:, :-1]
            y = mini_batch[:, -1:]
            a = self.forward(x)  #前向计算
            loss = self.loss(a, y)  #计算损失
            gradient_w, gradient_b = self.gradient(x, y)  #计算梯度
            self.update(gradient_w, gradient_b, eta)  #更新参数

将两部分改写的代码集成到Network类中的train函数中,最终的实现如下

import numpy as np

class Network(object):
    def __init__(self, num_of_weights):
        # 随机产生w的初始值
        # 为了保持程序每次运行结果的一致性,此处设置固定的随机数种子
        #np.random.seed(0)
        self.w = np.random.randn(num_of_weights, 1)
        self.b = 0.
        
    def forward(self, x):
        z = np.dot(x, self.w) + self.b
        return z
    
    def loss(self, z, y):
        error = z - y
        num_samples = error.shape[0]
        cost = error * error
        cost = np.sum(cost) / num_samples
        return cost
    
    def gradient(self, x, y):
        z = self.forward(x)
        N = x.shape[0]
        gradient_w = 1. / N * np.sum((z-y) * x, axis=0)
        gradient_w = gradient_w[:, np.newaxis]
        gradient_b = 1. / N * np.sum(z-y)
        return gradient_w, gradient_b
    
    def update(self, gradient_w, gradient_b, eta = 0.01):
        self.w = self.w - eta * gradient_w
        self.b = self.b - eta * gradient_b
            
                
    def train(self, training_data, num_epoches, batch_size=10, eta=0.01):
        n = len(training_data)
        losses = []
        for epoch_id in range(num_epoches):
            # 在每轮迭代开始之前,将训练数据的顺序随机打乱
            # 然后再按每次取batch_size条数据的方式取出
            np.random.shuffle(training_data)
            # 将训练数据进行拆分,每个mini_batch包含batch_size条的数据
            mini_batches = [training_data[k:k+batch_size] for k in range(0, n, batch_size)]
            for iter_id, mini_batch in enumerate(mini_batches):
                #print(self.w.shape)
                #print(self.b)
                x = mini_batch[:, :-1]
                y = mini_batch[:, -1:]
                a = self.forward(x)
                loss = self.loss(a, y)
                gradient_w, gradient_b = self.gradient(x, y)
                self.update(gradient_w, gradient_b, eta)
                losses.append(loss)
                print('Epoch {:3d} / iter {:3d}, loss = {:.4f}'.
                                 format(epoch_id, iter_id, loss))
        
        return losses

# 获取数据
train_data, test_data = load_data()

# 创建网络
net = Network(13)
# 启动训练
losses = net.train(train_data, num_epoches=50, batch_size=100, eta=0.1)

# 画出损失函数的变化趋势
plot_x = np.arange(len(losses))
plot_y = np.array(losses)
plt.plot(plot_x, plot_y)
plt.show()
输出:
Epoch   0 / iter   0, loss = 0.6273
Epoch   0 / iter   1, loss = 0.4835
Epoch   0 / iter   2, loss = 0.5830
Epoch   0 / iter   3, loss = 0.5466
Epoch   0 / iter   4, loss = 0.2147
Epoch   1 / iter   0, loss = 0.6645
Epoch   1 / iter   1, loss = 0.4875
Epoch   1 / iter   2, loss = 0.4707
Epoch   1 / iter   3, loss = 0.4153
Epoch   1 / iter   4, loss = 0.1402
Epoch   2 / iter   0, loss = 0.5897
Epoch   2 / iter   1, loss = 0.4373
Epoch   2 / iter   2, loss = 0.4631
Epoch   2 / iter   3, loss = 0.3960
Epoch   2 / iter   4, loss = 0.2340
Epoch   3 / iter   0, loss = 0.4139
Epoch   3 / iter   1, loss = 0.5635
Epoch   3 / iter   2, loss = 0.3807
Epoch   3 / iter   3, loss = 0.3975
Epoch   3 / iter   4, loss = 0.1207
Epoch   4 / iter   0, loss = 0.3786
Epoch   4 / iter   1, loss = 0.4474
Epoch   4 / iter   2, loss = 0.4019
Epoch   4 / iter   3, loss = 0.4352
Epoch   4 / iter   4, loss = 0.0435
Epoch   5 / iter   0, loss = 0.4387
Epoch   5 / iter   1, loss = 0.3886
Epoch   5 / iter   2, loss = 0.3182
Epoch   5 / iter   3, loss = 0.4189
Epoch   5 / iter   4, loss = 0.1741
Epoch   6 / iter   0, loss = 0.3191
Epoch   6 / iter   1, loss = 0.3601
Epoch   6 / iter   2, loss = 0.4199
Epoch   6 / iter   3, loss = 0.3289
Epoch   6 / iter   4, loss = 1.2691
Epoch   7 / iter   0, loss = 0.3202
Epoch   7 / iter   1, loss = 0.2855
Epoch   7 / iter   2, loss = 0.4129
Epoch   7 / iter   3, loss = 0.3331
Epoch   7 / iter   4, loss = 0.2218
Epoch   8 / iter   0, loss = 0.2368
Epoch   8 / iter   1, loss = 0.3457
Epoch   8 / iter   2, loss = 0.3339
Epoch   8 / iter   3, loss = 0.3812
Epoch   8 / iter   4, loss = 0.0534
Epoch   9 / iter   0, loss = 0.3567
Epoch   9 / iter   1, loss = 0.4033
Epoch   9 / iter   2, loss = 0.1926
Epoch   9 / iter   3, loss = 0.2803
Epoch   9 / iter   4, loss = 0.1557
Epoch  10 / iter   0, loss = 0.3435
Epoch  10 / iter   1, loss = 0.2790
Epoch  10 / iter   2, loss = 0.3456
Epoch  10 / iter   3, loss = 0.2076
Epoch  10 / iter   4, loss = 0.0935
Epoch  11 / iter   0, loss = 0.3024
Epoch  11 / iter   1, loss = 0.2517
Epoch  11 / iter   2, loss = 0.2797
Epoch  11 / iter   3, loss = 0.2989
Epoch  11 / iter   4, loss = 0.0301
Epoch  12 / iter   0, loss = 0.2507
Epoch  12 / iter   1, loss = 0.2563
Epoch  12 / iter   2, loss = 0.2971
Epoch  12 / iter   3, loss = 0.2833
Epoch  12 / iter   4, loss = 0.0597
Epoch  13 / iter   0, loss = 0.2827
Epoch  13 / iter   1, loss = 0.2094
Epoch  13 / iter   2, loss = 0.2417
Epoch  13 / iter   3, loss = 0.2985
Epoch  13 / iter   4, loss = 0.4036
Epoch  14 / iter   0, loss = 0.3085
Epoch  14 / iter   1, loss = 0.2015
Epoch  14 / iter   2, loss = 0.1830
Epoch  14 / iter   3, loss = 0.2978
Epoch  14 / iter   4, loss = 0.0630
Epoch  15 / iter   0, loss = 0.2342
Epoch  15 / iter   1, loss = 0.2780
Epoch  15 / iter   2, loss = 0.2571
Epoch  15 / iter   3, loss = 0.1838
Epoch  15 / iter   4, loss = 0.0627
Epoch  16 / iter   0, loss = 0.1896
Epoch  16 / iter   1, loss = 0.1966
Epoch  16 / iter   2, loss = 0.2018
Epoch  16 / iter   3, loss = 0.3257
Epoch  16 / iter   4, loss = 0.1268
Epoch  17 / iter   0, loss = 0.1990
Epoch  17 / iter   1, loss = 0.2031
Epoch  17 / iter   2, loss = 0.2662
Epoch  17 / iter   3, loss = 0.2128
Epoch  17 / iter   4, loss = 0.0133
Epoch  18 / iter   0, loss = 0.1780
Epoch  18 / iter   1, loss = 0.1575
Epoch  18 / iter   2, loss = 0.2547
Epoch  18 / iter   3, loss = 0.2544
Epoch  18 / iter   4, loss = 0.2007
Epoch  19 / iter   0, loss = 0.1657
Epoch  19 / iter   1, loss = 0.2000
Epoch  19 / iter   2, loss = 0.2045
Epoch  19 / iter   3, loss = 0.2524
Epoch  19 / iter   4, loss = 0.0632
Epoch  20 / iter   0, loss = 0.1629
Epoch  20 / iter   1, loss = 0.1895
Epoch  20 / iter   2, loss = 0.2523
Epoch  20 / iter   3, loss = 0.1896
Epoch  20 / iter   4, loss = 0.0918
Epoch  21 / iter   0, loss = 0.1583
Epoch  21 / iter   1, loss = 0.2322
Epoch  21 / iter   2, loss = 0.1567
Epoch  21 / iter   3, loss = 0.2089
Epoch  21 / iter   4, loss = 0.2035
Epoch  22 / iter   0, loss = 0.2273
Epoch  22 / iter   1, loss = 0.1427
Epoch  22 / iter   2, loss = 0.1712
Epoch  22 / iter   3, loss = 0.1826
Epoch  22 / iter   4, loss = 0.2878
Epoch  23 / iter   0, loss = 0.1685
Epoch  23 / iter   1, loss = 0.1622
Epoch  23 / iter   2, loss = 0.1499
Epoch  23 / iter   3, loss = 0.2329
Epoch  23 / iter   4, loss = 0.1486
Epoch  24 / iter   0, loss = 0.1617
Epoch  24 / iter   1, loss = 0.2083
Epoch  24 / iter   2, loss = 0.1442
Epoch  24 / iter   3, loss = 0.1740
Epoch  24 / iter   4, loss = 0.1641
Epoch  25 / iter   0, loss = 0.1159
Epoch  25 / iter   1, loss = 0.2064
Epoch  25 / iter   2, loss = 0.1690
Epoch  25 / iter   3, loss = 0.1778
Epoch  25 / iter   4, loss = 0.0159
Epoch  26 / iter   0, loss = 0.1730
Epoch  26 / iter   1, loss = 0.1861
Epoch  26 / iter   2, loss = 0.1387
Epoch  26 / iter   3, loss = 0.1486
Epoch  26 / iter   4, loss = 0.1090
Epoch  27 / iter   0, loss = 0.1393
Epoch  27 / iter   1, loss = 0.1775
Epoch  27 / iter   2, loss = 0.1564
Epoch  27 / iter   3, loss = 0.1245
Epoch  27 / iter   4, loss = 0.7611
Epoch  28 / iter   0, loss = 0.1470
Epoch  28 / iter   1, loss = 0.1211
Epoch  28 / iter   2, loss = 0.1285
Epoch  28 / iter   3, loss = 0.1854
Epoch  28 / iter   4, loss = 0.5240
Epoch  29 / iter   0, loss = 0.1740
Epoch  29 / iter   1, loss = 0.0898
Epoch  29 / iter   2, loss = 0.1392
Epoch  29 / iter   3, loss = 0.1842
Epoch  29 / iter   4, loss = 0.0251
Epoch  30 / iter   0, loss = 0.0978
Epoch  30 / iter   1, loss = 0.1529
Epoch  30 / iter   2, loss = 0.1640
Epoch  30 / iter   3, loss = 0.1503
Epoch  30 / iter   4, loss = 0.0975
Epoch  31 / iter   0, loss = 0.1399
Epoch  31 / iter   1, loss = 0.1595
Epoch  31 / iter   2, loss = 0.1209
Epoch  31 / iter   3, loss = 0.1203
Epoch  31 / iter   4, loss = 0.2008
Epoch  32 / iter   0, loss = 0.1501
Epoch  32 / iter   1, loss = 0.1310
Epoch  32 / iter   2, loss = 0.1065
Epoch  32 / iter   3, loss = 0.1489
Epoch  32 / iter   4, loss = 0.0818
Epoch  33 / iter   0, loss = 0.1401
Epoch  33 / iter   1, loss = 0.1367
Epoch  33 / iter   2, loss = 0.0970
Epoch  33 / iter   3, loss = 0.1481
Epoch  33 / iter   4, loss = 0.0711
Epoch  34 / iter   0, loss = 0.1157
Epoch  34 / iter   1, loss = 0.1050
Epoch  34 / iter   2, loss = 0.1378
Epoch  34 / iter   3, loss = 0.1505
Epoch  34 / iter   4, loss = 0.0429
Epoch  35 / iter   0, loss = 0.1096
Epoch  35 / iter   1, loss = 0.1279
Epoch  35 / iter   2, loss = 0.1715
Epoch  35 / iter   3, loss = 0.0888
Epoch  35 / iter   4, loss = 0.0473
Epoch  36 / iter   0, loss = 0.1350
Epoch  36 / iter   1, loss = 0.0781
Epoch  36 / iter   2, loss = 0.1458
Epoch  36 / iter   3, loss = 0.1288
Epoch  36 / iter   4, loss = 0.0421
Epoch  37 / iter   0, loss = 0.1083
Epoch  37 / iter   1, loss = 0.0972
Epoch  37 / iter   2, loss = 0.1513
Epoch  37 / iter   3, loss = 0.1236
Epoch  37 / iter   4, loss = 0.0366
Epoch  38 / iter   0, loss = 0.1204
Epoch  38 / iter   1, loss = 0.1341
Epoch  38 / iter   2, loss = 0.1109
Epoch  38 / iter   3, loss = 0.0905
Epoch  38 / iter   4, loss = 0.3906
Epoch  39 / iter   0, loss = 0.0923
Epoch  39 / iter   1, loss = 0.1094
Epoch  39 / iter   2, loss = 0.1295
Epoch  39 / iter   3, loss = 0.1239
Epoch  39 / iter   4, loss = 0.0684
Epoch  40 / iter   0, loss = 0.1188
Epoch  40 / iter   1, loss = 0.0984
Epoch  40 / iter   2, loss = 0.1067
Epoch  40 / iter   3, loss = 0.1057
Epoch  40 / iter   4, loss = 0.4602
Epoch  41 / iter   0, loss = 0.1478
Epoch  41 / iter   1, loss = 0.0980
Epoch  41 / iter   2, loss = 0.0921
Epoch  41 / iter   3, loss = 0.1020
Epoch  41 / iter   4, loss = 0.0430
Epoch  42 / iter   0, loss = 0.0991
Epoch  42 / iter   1, loss = 0.0994
Epoch  42 / iter   2, loss = 0.1270
Epoch  42 / iter   3, loss = 0.0988
Epoch  42 / iter   4, loss = 0.1176
Epoch  43 / iter   0, loss = 0.1286
Epoch  43 / iter   1, loss = 0.1013
Epoch  43 / iter   2, loss = 0.1066
Epoch  43 / iter   3, loss = 0.0779
Epoch  43 / iter   4, loss = 0.1481
Epoch  44 / iter   0, loss = 0.0840
Epoch  44 / iter   1, loss = 0.0858
Epoch  44 / iter   2, loss = 0.1388
Epoch  44 / iter   3, loss = 0.1000
Epoch  44 / iter   4, loss = 0.0313
Epoch  45 / iter   0, loss = 0.0896
Epoch  45 / iter   1, loss = 0.1173
Epoch  45 / iter   2, loss = 0.0916
Epoch  45 / iter   3, loss = 0.1043
Epoch  45 / iter   4, loss = 0.0074
Epoch  46 / iter   0, loss = 0.1008
Epoch  46 / iter   1, loss = 0.0915
Epoch  46 / iter   2, loss = 0.0877
Epoch  46 / iter   3, loss = 0.1139
Epoch  46 / iter   4, loss = 0.0292
Epoch  47 / iter   0, loss = 0.0679
Epoch  47 / iter   1, loss = 0.0987
Epoch  47 / iter   2, loss = 0.0929
Epoch  47 / iter   3, loss = 0.1098
Epoch  47 / iter   4, loss = 0.4838
Epoch  48 / iter   0, loss = 0.0693
Epoch  48 / iter   1, loss = 0.1095
Epoch  48 / iter   2, loss = 0.1128
Epoch  48 / iter   3, loss = 0.0890
Epoch  48 / iter   4, loss = 0.1008
Epoch  49 / iter   0, loss = 0.0724
Epoch  49 / iter   1, loss = 0.0804
Epoch  49 / iter   2, loss = 0.0919
Epoch  49 / iter   3, loss = 0.1233
Epoch  49 / iter   4, loss = 0.1849

百度飞桨-手把手带你零基础实践深度学习-课程笔记(一)_第18张图片

观察上述Loss的变化,随机梯度下降加快了训练过程,但由于每次仅基于少量样本更新参数和计算损失,所以损失下降曲线会出现震荡
说明:由于房价预测的数据量过少,所以难以感受到随机梯度下降带来的性能提升
总结
本节详细介绍了如何使用Numpy实现梯度下降算法,构建并训练了一个简单的线性模型实现波士顿房价预测,可以总结出,使用神经网络建模房价预测有三个要点:

  • 构建网络,初始化参数w和b,定义预测和损失函数的计算方法。
  • 随机选择初始点,建立梯度的计算方法和参数更新方式。
  • 从总的数据集中抽取部分数据作为一个mini_batch,计算梯度并更新参数,不断迭代直到损失函数几乎不再下降。

你可能感兴趣的:(paddlepaddle)