本文共 43040 字,大约阅读时间需要 143 分钟。
上一节我们初步认识了神经网络的基本概念(如神经元、多层连接、前向计算、计算图)和模型结构三要素(模型假设、评价函数和优化算法)。本节将以“波士顿房价”任务为例,向读者介绍使用Python语言和Numpy库来构建神经网络模型的思考过程和操作方法。
波士顿房价预测是一个经典的机器学习任务,类似于程序员世界的“Hello World”。和大家对房价的普遍认知相同,波士顿地区的房价是由诸多因素影响的。该数据集统计了13种可能影响房价的因素和该类型房屋的均价,期望构建一个基于13个因素进行房价预测的模型,如 图1 所示。
图1:波士顿房价影响因素示意图
对于预测问题,可以根据预测输出的类型是连续的实数值,还是离散的标签,区分为回归任务和分类任务。因为房价是一个连续值,所以房价预测显然是一个回归任务。下面我们尝试用最简单的线性回归模型解决这个问题,并用神经网络来实现这个模型。
假设房价和各影响因素之间能够用线性关系来描述:
模型的求解即是通过数据拟合出每个w和b。其中,wj和b分别表示该线性模型的权重和偏置。一维情况下,wj 和 b 是直线的斜率和截距。
线性回归模型使用均方误差作为损失函数(Loss),用以衡量预测房价和真实房价的差异,公式如下:
思考:
为什么要以均方误差作为损失函数?即将模型在每个训练样本上的预测误差加和,来衡量整体样本的准确性。这是因为损失函数的设计不仅仅要考虑“合理性”,同样需要考虑“易解性”,这个问题在后面的内容中会详细阐述。
神经网络的标准结构中每个神经元由加权和与非线性变换构成,然后将多个神经元分层的摆放并连接形成神经网络。线性回归模型可以认为是神经网络模型的一种极简特例,是一个只有加权和、没有非线性变换的神经元(无需形成网络),如 图2 所示。
图2:线性回归模型的神经网络结构
深度学习不仅实现了实现模型的端到端学习,还推动了人工智能进入工业大生产阶段,产生了标准化、自动化和模块化的通用框架。不同场景的深度学习模型具具备一定的通用性,五个步骤即可完成模型的构建和训练,如 图3 所示。
图3:构建神经网络/深度学习模型的基本步骤正是由于深度学习的建模和训练的过程存在通用性,在构建不同的模型时,只有模型三要素不同,其它步骤基本一致,深度学习框架才有用武之地。
数据处理包含五个部分:数据导入、数据形状变换、数据集划分、数据归一化处理和封装load data函数。数据预处理后,才能被模型调用。
说明:
本教程中的代码都可以在AIStudio上直接运行,Print结果都是基于程序真实运行的结果。
由于是真实案例,代码之前存在依赖关系,因此需要读者逐条、全部运行,否则会导致Print时报错。通过如下代码读入数据,了解下波士顿房价的数据集结构,数据存放在本地目录下housing.data文件中。
# 导入包import numpy as npimport json# 读入数据filedata ="/home/aistudio/data/data2943/housing.data"data=np.fromfile(filedata)print(data)
由于读入的原始数据是1维的,所有数据都连在一起。因此需要我们将数据的形状进行变换,形成一个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)data = data.reshape([data.shape[0] // feature_num, feature_num])# 查看数据x = data[0]print(x.shape)print(x)
将数据集划分成训练集和测试集,其中训练集用于确定模型的参数,测试集用于评判模型的效果。为什么要对数据集进行拆分,而不能直接应用于模型训练呢?这与学生时代的授课和考试关系比较类似,如 图4 所示。
图4:训练集和测试集拆分的意义上学时总有一些自作聪明的同学,平时不认真学习,考试前临阵抱佛脚,将习题死记硬背下来,但是成绩往往并不好。因为学校期望学生掌握的是知识,而不仅仅是习题本身。另出新的考题,才能鼓励学生努力去掌握习题背后的原理。同样我们期望模型学习的是任务的本质规律,而不是训练数据本身,模型训练未使用的数据,才能更真实的评估模型的效果。
在本案例中,我们将80%的数据用作训练集,20%用作测试集,实现代码如下。通过打印训练集的形状,可以发现共有404个样本,每个样本含有13个特征和1个预测值。
ratio = 0.8offset = int(data.shape[0] * ratio)training_data = data[:offset]training_data.shape
对每个特征进行归一化处理,使得每个特征的取值缩放到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函数,以便下一步模型的调用,代码如下。
def load_data(): # 从文件导入数据 datafile = './work/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] # 计算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]) # 训练集和测试集的划分比例 training_data = data[:offset] test_data = data[offset:] return training_data, test_data# 获取数据training_data, test_data = load_data()x = training_data[:, :-1]y = training_data[:, -1:]# 查看数据print(x[0])print(y[0])[-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][-0.00390539]
模型设计是深度学习模型关键要素之一,也称为网络结构设计,相当于模型的假设空间,即实现模型“前向计算”(从输入到输出)的过程。
如果将输入特征和输出预测值均以向量表示,输入特征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 = np.array(w).reshape([13, 1])
取出第1条样本数据,观察样本的特征向量与参数向量相乘的结果。
x1=x[0]t = np.dot(x1, w)print(t)[0.03395597]
完整的线性回归公式,还需要初始化偏移量bbb,同样随意赋初值-0.2。那么,线性回归模型的完整输出是z=t+bz=t+bz=t+b,这个从特征和参数计算输出值的过程称为“前向计算”。
b = -0.2z = t + bprint(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
表示的影响因素所对应的房价应该是zzz, 但实际数据告诉我们房价是yyy。这时我们需要有某种指标来衡量预测值zzz跟真实值yyy之间的差距。对于回归问题,最常采用的衡量方法是使用均方误差作为评价模型好坏的指标,具体定义如下:上式中的Loss(简记为: L)通常也被称作损失函数,它是衡量模型好坏的指标。在回归问题中均方误差是一种比较常见的形式,分类问题中通常会采用交叉熵作为损失函数,在后续的章节中会更详细的介绍。对一个样本计算损失的代码实现如下:
Loss = (y1 - z)*(y1 - z)print(Loss)[0.39428312]
因为计算损失时需要把每个样本的损失都考虑到,所以我们需要对单个样本的损失函数进行求和,并除以样本总数N。
在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类,可以方便的计算预测值和损失函数。需要注意的是,类中的变量xxx, www,bbb, zzz, errorerrorerror等均是向量。以变量xxx为例,共有两个维度,一个代表特征数量(值为13),一个代表样本数量,代码如下所示。
net = Network(13)# 此处可以一次性计算多个样本的预测值和损失函数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使得损失函数取得极小值。
我们先做一个小测试:如 图5 所示,基于微积分知识,求一条曲线在某个点的斜率等于函数该点的导数值。那么大家思考下,当处于曲线的极值点时,该点的斜率是多少?
图5:曲线斜率等于导数值
这个问题并不难回答,处于曲线极值点时的斜率为0,即函数在极值点处的导数为0。那么,让损失函数取极小值的www和bbb应该是下述方程组的解:
将样本数据(x,y)(x, y)(x,y)带入上面的方程组中即可求解出www和bbb的值,但是这种方法只对线性回归这样简单的任务有效。如果模型中含有非线性变换,或者损失函数不是均方差这种简单的形式,则很难通过上式求解。为了解决这个问题,下面我们将引入更加普适的数值求解方法:梯度下降法。
在现实中存在大量的函数正向求解容易,反向求解较难,被称为单向函数。这种函数在密码学中有大量的应用,密码锁的特点是可以迅速判断一个密钥是否是正确的(已知x,求y很容易),但是即使获取到密码锁系统,无法破解出正确的密钥是什么(已知y,求x很难)。
这种情况特别类似于一位想从山峰走到坡谷的盲人,他看不见坡谷在哪(无法逆向求解出$Loss&导数为0时的参数值),但可以伸脚探索身边的坡度(当前点的导数值,也称为梯度)。那么,求解Loss函数最小值可以“从当前的参数取值,一步步的按照下坡的方向下降,直到走到最低点”实现。这种方法笔者个人称它为“盲人下坡法”。哦不,有个更正式的说法“梯度下降法”。
训练的关键是找到一组(w,b),使得损失函数LLL取极小值。我们先看一下损失函数L只随两个参数w5
变化时的简单情形,启发下寻解的思路。 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)])#计算设定区域内每个参数取值所对应的Lossfor 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 pltfrom mpl_toolkits.mplot3d import Axes3Dfig = 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()
需要说明的是:为什么这里我们选择w5和w9来画图?这是因为选择这两个参数的时候,可比较直观的从损失函数的曲面图上发现极值点的存在。其他参数组合,从图形上观测损失函数的极值点不够直观。
观察上述曲线呈现出“圆滑”的坡度,这正是我们选择以均方误差作为损失函数的原因之一。图6 呈现了只有一个参数维度时,均方误差和绝对值误差(只将每个样本的误差累加,不做平方处理)的损失函数曲线图。
图6:均方误差和绝对值误差损失函数曲线图
由此可见,均方误差表现的“圆滑”的坡度有两个好处:
现在我们要找出一组[w5,w9]的值,使得损失函数最小,实现梯度下降法的方案如下:
图7:梯度下降方向示意图
上面我们讲过了损失函数的计算方法,这里稍微加以改写。为了梯度计算更加简洁,引入因子1/2
,定义损失函数如下:其中zi是网络对第i个样本的预测值:
梯度的定义:
可以计算出LLL对www和bbb的偏导数:从导数的计算过程可以看出,因子1/2 被消掉了,这是因为二次函数求导的时候会产生因子222,这也是我们将损失函数改写的原因。
下面我们考虑只有一个样本的情况下,计算梯度:
可以计算出L对w和b的偏导数:
可以通过具体的程序查看每个变量的数据和维度。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的所有权重的梯度,该方法读者可以自行实现。
基于Numpy广播机制(对向量和矩阵计算如同对1个单一变量计算一样),可以更快速的实现梯度计算。计算梯度的代码中直接用(z1 - y1) * x1,得到的是一个13维的向量,每个分量分别代表该维度的梯度。
gradient_w = (z1 - y1) * x1print('gradient_w_by_sample1 {}, gradient.shape {}'.format(gradient_w, gradient_w.shape))gradient_w_by_sample1 [ 0.25875126 -0.45417275 3.44214394 1.04441828 -0.15548386 -0.55875363 -0.09591377 0.09232085 3.03465138 1.43234507 3.49642036 -0.62581917 2.12068622], gradient.shape (13,)
输入数据中有多个样本,每个样本都对梯度有贡献。如上代码计算了只有样本1时的梯度值,同样的计算方法也可以计算样本2和样本3对梯度的贡献。
x2 = x[1]y2 = y[1]z2 = net.forward(x2)gradient_w = (z2 - y2) * x2print('gradient_w_by_sample2 {}, gradient.shape {}'.format(gradient_w, gradient_w.shape))gradient_w_by_sample2 [ 0.7329239 4.91417754 3.33394253 2.9912385 4.45673435 -0.58146277 -5.14623287 -2.4894594 7.19011988 7.99471607 0.83100061 -1.79236081 2.11028056], gradient.shape (13,)x3 = x[2]y3 = y[2]z3 = net.forward(x3)gradient_w = (z3 - y3) * x3print('gradient_w_by_sample3 {}, gradient.shape {}'.format(gradient_w, gradient_w.shape))gradient_w_by_sample3 [ 0.25138584 1.68549775 1.14349809 1.02595515 1.5286008 -1.93302947 0.4058236 -0.85385157 2.46611579 2.74208162 0.28502219 -0.46695229 2.39363651], gradient.shape (13,)
可能有的读者再次想到可以使用for循环把每个样本对梯度的贡献都计算出来,然后再作平均。但是我们不需要这么做,仍然可以使用Numpy的矩阵操作来简化运算,如3个样本的情况。
# 注意这里是一次取出3个样本的数据,不是取出第3个样本x3samples = x[0:3]y3samples = y[0:3]z3samples = net.forward(x3samples)print('x {}, shape {}'.format(x3samples, x3samples.shape))print('y {}, shape {}'.format(y3samples, y3samples.shape))print('z {}, shape {}'.format(z3samples, z3samples.shape))x [[-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] [-0.02122729 -0.14232673 -0.09655922 -0.08663366 -0.12907805 0.0168406 0.14904763 0.0721009 -0.20824365 -0.23154675 -0.02406783 0.0519112 -0.06111894] [-0.02122751 -0.14232673 -0.09655922 -0.08663366 -0.12907805 0.1632288 -0.03426854 0.0721009 -0.20824365 -0.23154675 -0.02406783 0.03943037 -0.20212336]], shape (3, 13)y [[-0.00390539] [-0.05723872] [ 0.23387239]], shape (3, 1)z [[-12.05947643] [-34.58467747] [-11.60858134]], shape (3, 1)
上面的x3samples, y3samples, z3samples的第一维大小均为3,表示有3个样本。下面计算这3个样本对梯度的贡献。
gradient_w = (z3samples - y3samples) * x3samplesprint('gradient_w {}, gradient.shape {}'.format(gradient_w, gradient_w.shape))gradient_w [[ 0.25875126 -0.45417275 3.44214394 1.04441828 -0.15548386 -0.55875363 -0.09591377 0.09232085 3.03465138 1.43234507 3.49642036 -0.62581917 2.12068622] [ 0.7329239 4.91417754 3.33394253 2.9912385 4.45673435 -0.58146277 -5.14623287 -2.4894594 7.19011988 7.99471607 0.83100061 -1.79236081 2.11028056] [ 0.25138584 1.68549775 1.14349809 1.02595515 1.5286008 -1.93302947 0.4058236 -0.85385157 2.46611579 2.74208162 0.28502219 -0.46695229 2.39363651]], gradient.shape (3, 13)
此处可见,计算梯度gradient_w的维度是3×13,并且其第1行与上面第1个样本计算的梯度gradient_w_by_sample1一致,第2行与上面第2个样本计算的梯度gradient_w_by_sample1一致,第3行与上面第3个样本计算的梯度gradient_w_by_sample1一致。这里使用矩阵操作,可能更加方便的对3个样本分别计算各自对梯度的贡献。
那么对于有N个样本的情形,我们可以直接使用如下方式计算出所有样本对梯度的贡献,这就是使用Numpy库广播功能带来的便捷。 小结一下这里使用Numpy库的广播功能:
一方面可以扩展参数的维度,代替for循环来计算1个样本对从w0 到w12 的所有参数的梯度。
另一方面可以扩展样本的维度,代替for循环来计算样本0到样本403对参数的梯度。
z = net.forward(x) gradient_w = (z - y) * x print('gradient_w shape {}'.format(gradient_w.shape)) print(gradient_w) gradient_w shape (404, 13) [[ 0.25875126 -0.45417275 3.44214394 ... 3.49642036 -0.62581917 2.12068622] [ 0.7329239 4.91417754 3.33394253 ... 0.83100061 -1.79236081 2.11028056] [ 0.25138584 1.68549775 1.14349809 ... 0.28502219 -0.46695229 2.39363651] ... [ 14.70025543 -15.10890735 36.23258734 ... 24.54882966 5.51071122 26.26098922] [ 9.29832217 -15.33146159 36.76629344 ... 24.91043398 -1.27564923 26.61808955] [ 19.55115919 -10.8177237 25.94192351 ... 17.5765494 3.94557661 17.64891012]]
上面gradient_w的每一行代表了一个样本对梯度的贡献。根据梯度的计算公式,总梯度是对每个样本对梯度贡献的平均值。
我们也可以使用Numpy的均值函数来完成此过程:
# axis = 0 表示把每一行做相加然后再除以总的行数gradient_w = np.mean(gradient_w, axis=0)print('gradient_w ', gradient_w.shape)print('w ', net.w.shape)print(gradient_w)print(net.w)gradient_w (13,)w (13, 1)[ 1.59697064 -0.92928123 4.72726926 1.65712204 4.96176389 1.18068454 4.55846519 -3.37770889 9.57465893 10.29870662 1.3900257 -0.30152215 1.09276043][[ 1.76405235e+00] [ 4.00157208e-01] [ 9.78737984e-01] [ 2.24089320e+00] [ 1.86755799e+00] [ 1.59000000e+02] [ 9.50088418e-01] [-1.51357208e-01] [-1.03218852e-01] [ 1.59000000e+02] [ 1.44043571e-01] [ 1.45427351e+00] [ 7.61037725e-01]]
我们使用numpy的矩阵操作方便的完成了gradient的计算,但引入了一个问题,gradient_w的形状是(13,),而w的维度是(13, 1)。导致该问题的原因是使用np.mean函数的时候消除了第0维。为了加减乘除等计算方便,gradient_w和w必须保持一致的形状。因此我们将gradient_w的维度也设置为(13, 1),代码如下:
gradient_w = gradient_w[:, np.newaxis]print('gradient_w shape', gradient_w.shape)gradient_w shape (13, 1)
综合上面的讨论,计算梯度的代码如下所示。
z = net.forward(x)gradient_w = (z - y) * xgradient_w = np.mean(gradient_w, axis=0)gradient_w = gradient_w[:, np.newaxis]gradient_warray([[ 1.59697064], [-0.92928123], [ 4.72726926], [ 1.65712204], [ 4.96176389], [ 1.18068454], [ 4.55846519], [-3.37770889], [ 9.57465893], [10.29870662], [ 1.3900257 ], [-0.30152215], [ 1.09276043]])
上述代码非常简洁的完成了www的梯度计算。同样,计算bbb的梯度的代码也是类似的原理。
gradient_b = (z - y)gradient_b = np.mean(gradient_b)# 此处b是一个数值,所以可以直接用np.mean得到一个标量gradient_b-1.0918438870293816e-13
将上面计算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.0net.w[9] = -100.0z = 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.3005008179159gradient [-0.850073323995813, -6.138412364807849]
下面我们开始研究更新梯度的方法。首先沿着梯度的反方向移动一小步,找到下一个点P1,观察损失函数的变化。
# 在[w5, w9]平面上,沿着梯度的反方向移动到下一个点P1# 定义移动步长 etaeta = 0.1# 更新参数w5和w9net.w[5] = net.w[5] - eta * gradient_w5net.w[9] = net.w[9] - eta * gradient_w9# 重新计算z和lossz = 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.6472185028845gradient [-0.8556356178645292, -6.0932268634065805]
运行上面的代码,可以发现沿着梯度反方向走一小步,下一个点的损失函数的确减少了。感兴趣的话,大家可以尝试不停的点击上面的代码块,观察损失函数是否一直在变小。
在上述代码中,每次更新参数使用的语句: net.w[5] = net.w[5] - eta * gradient_w5
如 图8 所示,特征输入归一化后,不同参数输出的Loss是一个比较规整的曲线,学习率可以设置成统一的值 ;特征输入未归一化时,不同特征对应的参数所需的步长不一致,尺度较大的参数需要大步长,尺寸较小的参数需要小步长,导致无法设置统一的学习率。
图8:未归一化的特征,会导致不同特征维度的理想步长不同
将上面的循环的计算过程封装在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.3005008179159iter 50, point [-99.56362583488914, -96.92631128470325], loss 649.221346830939iter 100, point [-99.13580802595692, -94.02279509580971], loss 614.6970095624063iter 150, point [-98.7079902170247, -91.22404911807594], loss 582.543755023494iter 200, point [-98.28017240809248, -88.52620357520894], loss 552.5911329872217iter 250, point [-97.85235459916026, -85.9255316243737], loss 524.6810152322887iter 300, point [-97.42453679022805, -83.41844407682491], loss 498.6667034691001iter 350, point [-96.99671898129583, -81.00148431353688], loss 474.4121018974464iter 400, point [-96.56890117236361, -78.67132338862874], loss 451.7909497114133iter 450, point [-96.14108336343139, -76.42475531364933], loss 430.68610920670284iter 500, point [-95.71326555449917, -74.25869251604028], loss 410.988905460488iter 550, point [-95.28544774556696, -72.17016146534513], loss 392.5985138460824iter 600, point [-94.85762993663474, -70.15629846096763], loss 375.4213919156372iter 650, point [-94.42981212770252, -68.21434557551346], loss 359.3707524354014iter 700, point [-94.0019943187703, -66.34164674796719], loss 344.36607459115214iter 750, point [-93.57417650983808, -64.53564402117185], loss 330.33265059761464iter 800, point [-93.14635870090586, -62.793873918279786], loss 317.2011651461846iter 850, point [-92.71854089197365, -61.11396395304264], loss 304.907305311265iter 900, point [-92.29072308304143, -59.49362926899678], loss 293.3913987080144iter 950, point [-91.86290527410921, -57.930669402782904], loss 282.5980778542974iter 1000, point [-91.43508746517699, -56.4229651670156], loss 272.47596883802515iter 1050, point [-91.00726965624477, -54.968475648286564], loss 262.9774025287022iter 1100, point [-90.57945184731255, -53.56523531604897], loss 254.05814669965383iter 1150, point [-90.15163403838034, -52.21135123828792], loss 245.67715754581488iter 1200, point [-89.72381622944812, -50.90500040003218], loss 237.796349191773iter 1250, point [-89.2959984205159, -49.6444271209092], loss 230.3803798866218iter 1300, point [-88.86818061158368, -48.42794056808474], loss 223.3964536766492iter 1350, point [-88.44036280265146, -47.2539123610643], loss 216.81413643451378iter 1400, point [-88.01254499371925, -46.12077426496303], loss 210.60518520483126iter 1450, point [-87.58472718478703, -45.027015968976976], loss 204.74338990147896iter 1500, point [-87.15690937585481, -43.9711829469081], loss 199.20442646183588iter 1550, point [-86.72909156692259, -42.95187439671279], loss 193.96572062803054iter 1600, point [-86.30127375799037, -41.96774125615467], loss 189.00632158541163iter 1650, point [-85.87345594905815, -41.017484291751295], loss 184.3067847442463iter 1700, point [-85.44563814012594, -40.0998522583068], loss 179.84906300239203iter 1750, point [-85.01782033119372, -39.21364012642417], loss 175.61640587468244iter 1800, point [-84.5900025222615, -38.35768737548557], loss 171.59326591927967iter 1850, point [-84.16218471332928, -37.530876349682856], loss 167.76521193253296iter 1900, point [-83.73436690439706, -36.73213067476985], loss 164.11884842217898iter 1950, point [-83.30654909546485, -35.96041373329276], loss 160.64174090423475
在上述程序中,每次损失函数和梯度计算都是基于数据集中的全量数据。对于波士顿房价预测任务数据集而言,样本数比较少,只有404个。但在实际问题中,数据集往往非常大,如果每次都使用全量数据进行计算,效率非常低,通俗的说就是“杀鸡焉用牛刀”。由于参数每次只沿着梯度反方向更新一点点,因此方向并不需要那么精确。一个合理的解决方案是每次从总的数据集中随机抽取出小部分数据来代表整体,基于这部分数据计算梯度和损失来更新参数,这种方法被称作随机梯度下降法(Stochastic Gradient Descent,SGD),核心概念如下:
数据处理需要实现拆分数据批次和样本乱序(为了实现随机抽样的效果)两个功能。
# 获取数据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分成 104/40+1=41 个 mini_batch了,其中前40个mini_batch,每个均含有10个样本,最后一个mini_batch只含有4个样本。
batch_size = 10n = 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 41first mini_batch shape (10, 14)last mini_batch shape (4, 14)
另外,我们这里是按顺序取出mini_batch的,而SGD里面是随机的抽取一部分样本代表总体。为了实现随机抽样的效果,我们先将train_data里面的样本顺序随机打乱,然后再抽取mini_batch。随机打乱样本顺序,需要用到np.random.shuffle函数,下面先介绍它的用法。
说明:
通过大量实验发现,模型对最后出现的数据印象更加深刻。训练数据导入后,越接近模型训练结束,最后几个批次数据对模型参数的影响越大。为了避免模型记忆影响训练效果,需要进行样本乱序操作。
# 新建一个arraya = np.array([1,2,3,4,5,6,7,8,9,10,11,12])print('before shuffle', a)np.random.shuffle(a)print('after shuffle', a)before shuffle [ 1 2 3 4 5 6 7 8 9 10 11 12]after shuffle [ 7 2 11 3 8 6 12 1 4 5 10 9]
多次运行上面的代码,可以发现每次执行shuffle函数后的数字顺序均不同。 上面举的是一个1维数组乱序的案例,我们在观察下2维数组乱序后的效果。
# 新建一个arraya = 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_batchbatch_size = 10n = 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数据输入到模型中用于参数训练。训练过程的核心是两层循环:
1:第一层循环,代表样本集合要被训练遍历几次,称为“epoch”,代码如下:
for epoch_id in range(num_epoches): 2:第二层循环,代表每次遍历时,样本集合被拆分成的多个批次,需要全部执行训练,称为“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 npclass 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.6273Epoch 0 / iter 1, loss = 0.4835Epoch 0 / iter 2, loss = 0.5830Epoch 0 / iter 3, loss = 0.5466Epoch 0 / iter 4, loss = 0.2147Epoch 1 / iter 0, loss = 0.6645Epoch 1 / iter 1, loss = 0.4875Epoch 1 / iter 2, loss = 0.4707Epoch 1 / iter 3, loss = 0.4153Epoch 1 / iter 4, loss = 0.1402Epoch 2 / iter 0, loss = 0.5897Epoch 2 / iter 1, loss = 0.4373Epoch 2 / iter 2, loss = 0.4631Epoch 2 / iter 3, loss = 0.3960Epoch 2 / iter 4, loss = 0.2340Epoch 3 / iter 0, loss = 0.4139Epoch 3 / iter 1, loss = 0.5635Epoch 3 / iter 2, loss = 0.3807Epoch 3 / iter 3, loss = 0.3975Epoch 3 / iter 4, loss = 0.1207Epoch 4 / iter 0, loss = 0.3786Epoch 4 / iter 1, loss = 0.4474Epoch 4 / iter 2, loss = 0.4019Epoch 4 / iter 3, loss = 0.4352Epoch 4 / iter 4, loss = 0.0435Epoch 5 / iter 0, loss = 0.4387Epoch 5 / iter 1, loss = 0.3886Epoch 5 / iter 2, loss = 0.3182Epoch 5 / iter 3, loss = 0.4189Epoch 5 / iter 4, loss = 0.1741Epoch 6 / iter 0, loss = 0.3191Epoch 6 / iter 1, loss = 0.3601Epoch 6 / iter 2, loss = 0.4199Epoch 6 / iter 3, loss = 0.3289Epoch 6 / iter 4, loss = 1.2691Epoch 7 / iter 0, loss = 0.3202Epoch 7 / iter 1, loss = 0.2855Epoch 7 / iter 2, loss = 0.4129Epoch 7 / iter 3, loss = 0.3331Epoch 7 / iter 4, loss = 0.2218Epoch 8 / iter 0, loss = 0.2368Epoch 8 / iter 1, loss = 0.3457Epoch 8 / iter 2, loss = 0.3339Epoch 8 / iter 3, loss = 0.3812Epoch 8 / iter 4, loss = 0.0534Epoch 9 / iter 0, loss = 0.3567Epoch 9 / iter 1, loss = 0.4033Epoch 9 / iter 2, loss = 0.1926Epoch 9 / iter 3, loss = 0.2803Epoch 9 / iter 4, loss = 0.1557Epoch 10 / iter 0, loss = 0.3435Epoch 10 / iter 1, loss = 0.2790Epoch 10 / iter 2, loss = 0.3456Epoch 10 / iter 3, loss = 0.2076Epoch 10 / iter 4, loss = 0.0935Epoch 11 / iter 0, loss = 0.3024Epoch 11 / iter 1, loss = 0.2517Epoch 11 / iter 2, loss = 0.2797Epoch 11 / iter 3, loss = 0.2989Epoch 11 / iter 4, loss = 0.0301Epoch 12 / iter 0, loss = 0.2507Epoch 12 / iter 1, loss = 0.2563Epoch 12 / iter 2, loss = 0.2971Epoch 12 / iter 3, loss = 0.2833Epoch 12 / iter 4, loss = 0.0597Epoch 13 / iter 0, loss = 0.2827Epoch 13 / iter 1, loss = 0.2094Epoch 13 / iter 2, loss = 0.2417Epoch 13 / iter 3, loss = 0.2985Epoch 13 / iter 4, loss = 0.4036Epoch 14 / iter 0, loss = 0.3085Epoch 14 / iter 1, loss = 0.2015Epoch 14 / iter 2, loss = 0.1830Epoch 14 / iter 3, loss = 0.2978Epoch 14 / iter 4, loss = 0.0630Epoch 15 / iter 0, loss = 0.2342Epoch 15 / iter 1, loss = 0.2780Epoch 15 / iter 2, loss = 0.2571Epoch 15 / iter 3, loss = 0.1838Epoch 15 / iter 4, loss = 0.0627Epoch 16 / iter 0, loss = 0.1896Epoch 16 / iter 1, loss = 0.1966Epoch 16 / iter 2, loss = 0.2018Epoch 16 / iter 3, loss = 0.3257Epoch 16 / iter 4, loss = 0.1268Epoch 17 / iter 0, loss = 0.1990Epoch 17 / iter 1, loss = 0.2031Epoch 17 / iter 2, loss = 0.2662Epoch 17 / iter 3, loss = 0.2128Epoch 17 / iter 4, loss = 0.0133Epoch 18 / iter 0, loss = 0.1780Epoch 18 / iter 1, loss = 0.1575Epoch 18 / iter 2, loss = 0.2547Epoch 18 / iter 3, loss = 0.2544Epoch 18 / iter 4, loss = 0.2007Epoch 19 / iter 0, loss = 0.1657Epoch 19 / iter 1, loss = 0.2000Epoch 19 / iter 2, loss = 0.2045Epoch 19 / iter 3, loss = 0.2524Epoch 19 / iter 4, loss = 0.0632Epoch 20 / iter 0, loss = 0.1629Epoch 20 / iter 1, loss = 0.1895Epoch 20 / iter 2, loss = 0.2523Epoch 20 / iter 3, loss = 0.1896Epoch 20 / iter 4, loss = 0.0918Epoch 21 / iter 0, loss = 0.1583Epoch 21 / iter 1, loss = 0.2322Epoch 21 / iter 2, loss = 0.1567Epoch 21 / iter 3, loss = 0.2089Epoch 21 / iter 4, loss = 0.2035Epoch 22 / iter 0, loss = 0.2273Epoch 22 / iter 1, loss = 0.1427Epoch 22 / iter 2, loss = 0.1712Epoch 22 / iter 3, loss = 0.1826Epoch 22 / iter 4, loss = 0.2878Epoch 23 / iter 0, loss = 0.1685Epoch 23 / iter 1, loss = 0.1622Epoch 23 / iter 2, loss = 0.1499Epoch 23 / iter 3, loss = 0.2329Epoch 23 / iter 4, loss = 0.1486Epoch 24 / iter 0, loss = 0.1617Epoch 24 / iter 1, loss = 0.2083Epoch 24 / iter 2, loss = 0.1442Epoch 24 / iter 3, loss = 0.1740Epoch 24 / iter 4, loss = 0.1641Epoch 25 / iter 0, loss = 0.1159Epoch 25 / iter 1, loss = 0.2064Epoch 25 / iter 2, loss = 0.1690Epoch 25 / iter 3, loss = 0.1778Epoch 25 / iter 4, loss = 0.0159Epoch 26 / iter 0, loss = 0.1730Epoch 26 / iter 1, loss = 0.1861Epoch 26 / iter 2, loss = 0.1387Epoch 26 / iter 3, loss = 0.1486Epoch 26 / iter 4, loss = 0.1090Epoch 27 / iter 0, loss = 0.1393Epoch 27 / iter 1, loss = 0.1775Epoch 27 / iter 2, loss = 0.1564Epoch 27 / iter 3, loss = 0.1245Epoch 27 / iter 4, loss = 0.7611Epoch 28 / iter 0, loss = 0.1470Epoch 28 / iter 1, loss = 0.1211Epoch 28 / iter 2, loss = 0.1285Epoch 28 / iter 3, loss = 0.1854Epoch 28 / iter 4, loss = 0.5240Epoch 29 / iter 0, loss = 0.1740Epoch 29 / iter 1, loss = 0.0898Epoch 29 / iter 2, loss = 0.1392Epoch 29 / iter 3, loss = 0.1842Epoch 29 / iter 4, loss = 0.0251Epoch 30 / iter 0, loss = 0.0978Epoch 30 / iter 1, loss = 0.1529Epoch 30 / iter 2, loss = 0.1640Epoch 30 / iter 3, loss = 0.1503Epoch 30 / iter 4, loss = 0.0975Epoch 31 / iter 0, loss = 0.1399Epoch 31 / iter 1, loss = 0.1595Epoch 31 / iter 2, loss = 0.1209Epoch 31 / iter 3, loss = 0.1203Epoch 31 / iter 4, loss = 0.2008Epoch 32 / iter 0, loss = 0.1501Epoch 32 / iter 1, loss = 0.1310Epoch 32 / iter 2, loss = 0.1065Epoch 32 / iter 3, loss = 0.1489Epoch 32 / iter 4, loss = 0.0818Epoch 33 / iter 0, loss = 0.1401Epoch 33 / iter 1, loss = 0.1367Epoch 33 / iter 2, loss = 0.0970Epoch 33 / iter 3, loss = 0.1481Epoch 33 / iter 4, loss = 0.0711Epoch 34 / iter 0, loss = 0.1157Epoch 34 / iter 1, loss = 0.1050Epoch 34 / iter 2, loss = 0.1378Epoch 34 / iter 3, loss = 0.1505Epoch 34 / iter 4, loss = 0.0429Epoch 35 / iter 0, loss = 0.1096Epoch 35 / iter 1, loss = 0.1279Epoch 35 / iter 2, loss = 0.1715Epoch 35 / iter 3, loss = 0.0888Epoch 35 / iter 4, loss = 0.0473Epoch 36 / iter 0, loss = 0.1350Epoch 36 / iter 1, loss = 0.0781Epoch 36 / iter 2, loss = 0.1458Epoch 36 / iter 3, loss = 0.1288Epoch 36 / iter 4, loss = 0.0421Epoch 37 / iter 0, loss = 0.1083Epoch 37 / iter 1, loss = 0.0972Epoch 37 / iter 2, loss = 0.1513Epoch 37 / iter 3, loss = 0.1236Epoch 37 / iter 4, loss = 0.0366Epoch 38 / iter 0, loss = 0.1204Epoch 38 / iter 1, loss = 0.1341Epoch 38 / iter 2, loss = 0.1109Epoch 38 / iter 3, loss = 0.0905Epoch 38 / iter 4, loss = 0.3906Epoch 39 / iter 0, loss = 0.0923Epoch 39 / iter 1, loss = 0.1094Epoch 39 / iter 2, loss = 0.1295Epoch 39 / iter 3, loss = 0.1239Epoch 39 / iter 4, loss = 0.0684Epoch 40 / iter 0, loss = 0.1188Epoch 40 / iter 1, loss = 0.0984Epoch 40 / iter 2, loss = 0.1067Epoch 40 / iter 3, loss = 0.1057Epoch 40 / iter 4, loss = 0.4602Epoch 41 / iter 0, loss = 0.1478Epoch 41 / iter 1, loss = 0.0980Epoch 41 / iter 2, loss = 0.0921Epoch 41 / iter 3, loss = 0.1020Epoch 41 / iter 4, loss = 0.0430Epoch 42 / iter 0, loss = 0.0991Epoch 42 / iter 1, loss = 0.0994Epoch 42 / iter 2, loss = 0.1270Epoch 42 / iter 3, loss = 0.0988Epoch 42 / iter 4, loss = 0.1176Epoch 43 / iter 0, loss = 0.1286Epoch 43 / iter 1, loss = 0.1013Epoch 43 / iter 2, loss = 0.1066Epoch 43 / iter 3, loss = 0.0779Epoch 43 / iter 4, loss = 0.1481Epoch 44 / iter 0, loss = 0.0840Epoch 44 / iter 1, loss = 0.0858Epoch 44 / iter 2, loss = 0.1388Epoch 44 / iter 3, loss = 0.1000Epoch 44 / iter 4, loss = 0.0313Epoch 45 / iter 0, loss = 0.0896Epoch 45 / iter 1, loss = 0.1173Epoch 45 / iter 2, loss = 0.0916Epoch 45 / iter 3, loss = 0.1043Epoch 45 / iter 4, loss = 0.0074Epoch 46 / iter 0, loss = 0.1008Epoch 46 / iter 1, loss = 0.0915Epoch 46 / iter 2, loss = 0.0877Epoch 46 / iter 3, loss = 0.1139Epoch 46 / iter 4, loss = 0.0292Epoch 47 / iter 0, loss = 0.0679Epoch 47 / iter 1, loss = 0.0987Epoch 47 / iter 2, loss = 0.0929Epoch 47 / iter 3, loss = 0.1098Epoch 47 / iter 4, loss = 0.4838Epoch 48 / iter 0, loss = 0.0693Epoch 48 / iter 1, loss = 0.1095Epoch 48 / iter 2, loss = 0.1128Epoch 48 / iter 3, loss = 0.0890Epoch 48 / iter 4, loss = 0.1008Epoch 49 / iter 0, loss = 0.0724Epoch 49 / iter 1, loss = 0.0804Epoch 49 / iter 2, loss = 0.0919Epoch 49 / iter 3, loss = 0.1233Epoch 49 / iter 4, loss = 0.1849
说明:
由于房价预测的数据量过少,所以难以感受到随机梯度下降带来的性能提升。
总结
本节,我们详细讲解了如何使用Numpy实现梯度下降算法,构建并训练了一个简单的线性模型实现波士顿房价预测,可以总结出,使用神经网络建模房价预测有三个要点:基本知识
链式法则是微积分中的求导法则,用于求一个复合函数的导数,是在微积分的求导运算中一种常用的方法。复合函数的导数将是构成复合这有限个函数在相应点的导数的乘积,就像锁链一样一环套一环,故称链式法则。如 图9 所示,如果求最终输出对内层输入(第一层)的梯度,等于外层梯度(第二层)乘以本层函数的梯度。
图9:求导的链式法则
(1)为何是反向计算梯度?即梯度是由网络后端向前端计算。当前层的梯度要依据处于网络中后一层的梯度来计算,所以只有先算后一层的梯度才能计算本层的梯度。
(2)案例:购买苹果产生消费的计算图。假设一家商店9折促销苹果,每个的单价100元。计算一个顾客总消费的结构如 图10 所示。
图10:购买苹果所产生的消费计算图
转载地址:http://vaqms.baihongyu.com/