百度PaddlePaddle入门-2(使用Numpy构建神经网络)
(转载自PaddlePaddle培训课程,https://aistudio.baidu.com)
使用Numpy构建神经网络
本节将使用Python语言和Numpy库来构建神经网络模型,向读者展示神经网络的基本概念和工作过程。
波士顿房价预测
波士顿房价预测是一个经典的机器学习问题,类似于程序员世界的“Hello World”。波士顿地区的房价是由诸多因素影响的,该数据集统计了13种可能影响房价的因素和该类型房屋的均价,期望构建一个基于13个因素预测房价的模型。预测问题根据预测输出的类型是连续的实数值,还是离散的标签,区分为回归任务和分类任务。因为房价是一个连续值,所以房价预测显然是一个回归任务。下面我们尝试用最简单的线性回归模型解决这个问题,并用神经网络来实现这个模型。
线性回归模型
假设房价和各影响因素之间能够用线性关系来描述(类似牛顿第二定律的案例):
y=Sigma(XjWj+b) (j=[1,M])
模型的求解即是通过数据拟合出每个wj和b。
数据准备
在搭建模型之前,让我们先导入数据,查阅下内容。房价数据存放在本地目录下的housing.data文件中,通过执行如下的代码可以导入数据并查阅。
1 # 导入需要用到的package 2 import numpy as np 3 import json 4 # 读入训练数据 5 datafile = './work/housing.data' 6 data = np.fromfile(datafile, sep=' ') 7 data
打印data输出:
array([6.320e-03, 1.800e+01, 2.310e+00, ..., 3.969e+02, 7.880e+00,
1.190e+01])
data.shape=[7084,],这样得到data.shape[0]=7084;shape与shape[0]是不同的。
因为读入的原始数据是1维的,所有数据都连在了一起。所以将数据的形状进行变换,形成一个2维的矩阵。每行为一个数据样本(14个值),每个数据样本包含13个X(影响房价的特征)和一个Y(该类型房屋的均价)。
经过下面的操作之后,得到data.shape=[506,14].
1 # 读入之后的数据被转化成1维array,其中array的 2 # 第0-13项是第一条数据,第14-27项是第二条数据,.... 3 # 这里对原始数据做reshape,变成N x 14的形式 4 feature_names = [ 'CRIM', 'ZN', 'INDUS', 'CHAS', 'NOX', 'RM', 'AGE','DIS', 5 'RAD', 'TAX', 'PTRATIO', 'B', 'LSTAT', 'MEDV' ] 6 feature_num = len(feature_names) 7 data = data.reshape([data.shape[0] // feature_num, feature_num]
1 # 查看数据 2 x = data[0] 3 print(x.shape) 4 print(x) 5 print(x[1]) 6 print(len(data))
(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] 18.0 506
目前data的数据结构是506行14列;x=data[0]得到data的第一行数据;x[1]是第二列数据18.00.
取80%的数据作为训练集,预留20%的数据用于测试模型的预测效果(训练好的模型预测值与实际房价的差距)。打印训练集的形状可见,我们共有404个样本,每个样本含有13个特征和1个预测值。
1 ratio = 0.8 2 offset = int(data.shape[0] * ratio) 3 print("offset= ",offset) 4 training_data = data[:offset] 5 training_data.shape
offset= 404
(404, 14)
上面得到traning_data是404行14列数据。
对每个特征进行归一化处理,使得每个特征的取值缩放到0~1之间。这样做有两个好处:
- 模型训练更高效(后面解释)。
- 特征前的权重大小可代表该变量对预测结果的贡献度(因为每个特征值本身的范围相同)
1 # 计算train数据集的最大值,最小值,平均值(以列为单位统计) 2 maximums, minimums, avgs = \ 3 training_data.max(axis=0), \ 4 training_data.min(axis=0), \ 5 training_data.sum(axis=0) / training_data.shape[0] 6 # 对数据进行归一化处理 7 for i in range(feature_num): 8 print(maximums[i], minimums[i], avgs[i]) 9 data[:, i] = (data[:, i] - avgs[i]) / (maximums[i] - minimums[i])
88.9762 0.00632 1.9158993069306929 100.0 0.0 14.232673267326733 25.65 0.46 9.502326732673243 1.0 0.0 0.08663366336633663 0.871 0.385 0.5317319306930701 8.78 3.561 6.3331089108910925 100.0 2.9 64.42747524752477 12.1265 1.1296 4.174213613861384 24.0 1.0 6.78960396039604 666.0 187.0 352.91089108910893 22.0 12.6 18.026237623762338 396.9 70.8 379.97175742574177 37.97 1.73 11.354950495049506 50.0 5.0 24.175742574257452
(axis=0表示对列处理)上面操作可以得到每一列的最大,最小及均值。同时也对数据作了归一化。
将上述几个数据处理操作合并成load data函数,并确认函数的执行效果。
1 def load_data(): 2 # 从文件导入数据 3 datafile = './work/housing.data' 4 data = np.fromfile(datafile, sep=' ') 5 6 # 每条数据包括14项,其中前面13项是影响因素,第14项是相应的房屋价格中位数 7 feature_names = [ 'CRIM', 'ZN', 'INDUS', 'CHAS', 'NOX', 'RM', 'AGE', \ 8 'DIS', 'RAD', 'TAX', 'PTRATIO', 'B', 'LSTAT', 'MEDV' ] 9 feature_num = len(feature_names) 10 11 # 将原始数据进行Reshape,变成[N, 14]这样的形状:N行14列 12 data = data.reshape([data.shape[0] // feature_num, feature_num]) 13 14 # 将原数据集拆分成训练集和测试集 15 # 这里使用80%的数据做训练,20%的数据做测试 16 # 测试集和训练集必须是没有交集的 17 ratio = 0.8 18 offset = int(data.shape[0] * ratio) 19 training_data = data[:offset] 20 21 # 计算train数据集的最大值,最小值,平均值:axis=0,表示以列为主 22 maximums, minimums, avgs = training_data.max(axis=0), training_data.min(axis=0), \ 23 training_data.sum(axis=0) / training_data.shape[0] 24 25 # 对数据进行归一化处理 26 for i in range(feature_num): 27 #print(maximums[i], minimums[i], avgs[i]) 28 data[:, i] = (data[:, i] - avgs[i]) / (maximums[i] - minimums[i]) 29 30 # 训练集和测试集的划分比例 31 #ratio = 0.8 32 #offset = int(data.shape[0] * ratio) 33 training_data = data[:offset] 34 test_data = data[offset:] 35 return training_data, test_data
1 # 获取数据 2 training_data, test_data = load_data() 3 x = training_data[:, :-1] 4 y = training_data[:, -1:]
X取值是前13列,Y取值是最后一列。上面所示:测试集和训练集是没有交集的。
1 # 查看数据 2 print(x[0]) 3 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]
分别输出影响房价的13个信息和真实的房价。
如果将输入特征和输出预测值均以向量表示,输入特征x一共有13个分量,y只有1个分量,所以参数权重的形状(shape)应该是13 x 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]。也就是随机给权重赋值。
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] 2 #13行1列 3 w = np.array(w).reshape([13, 1]) 4 print(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. ]]
每一行都带一个【】。
取出第1条样本数据,观察样本的特征向量与参数向量相乘之后的结果。
1 x1=x[0] 2 print(x1) 3 z = np.dot(x1, w) 4 print(z)
[-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.03395597]
最终结果Z就是每一个参数与对应的权重相乘之和,也就是点积。
此外,完整的线性回归公式,还需要初始化偏移量b,同样随意赋初值-0.2。 那么,线性回归模型的完整输出是a=z+b,这个从特征和参数计算输出值的过程称为“前向计算”。
1 b = -0.2 2 a = z + b 3 print(a)
[-0.16604403]
这就是我们得到的房价预测值。
构建神经网络
将上述计算预测输出的过程以“类和对象”的方式来描述,实现的方案如下所示。类成员变量有参数 w 和 b,并写了一个forward函数(代表“前向计算”)完成上述从特征和参数到输出预测值的计算过程。
1 class Network(object): 2 def __init__(self, num_of_weights): 3 # 随机产生w的初始值 4 # 为了保持程序每次运行结果的一致性, 5 # 此处设置固定的随机数种子 6 np.random.seed(0) 7 self.w = np.random.randn(num_of_weights, 1) 8 self.b = 0. 9 10 def forward(self, x): 11 a = np.dot(x, self.w) + self.b 12 return a
基于Network类的定义,模型的计算过程可以按下述方式达成。
1 net = Network(13) 2 x1 = x[0] 3 y1 = y[0] 4 z = net.forward(x1) 5 print(x1) 6 print(y1) 7 print(z)
[-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] [-0.63182506]
13表示参数个数,x1第一组参数;y1第一组实际房价;z是根据第一组参数得到预测值。上图看到,预测值z与真实值y1之间还是有很大的差距。
通过模型计算x1表示的影响因素所对应的房价应该是z, 但实际数据告诉我们房价是y,这时我们需要有某种指标来衡量预测值z跟真实值y之间的差距。对于回归问题,最常采用的衡量方法是使用均方误差作为评价模型好坏的指标,具体定义如下:
Loss=(y - z) * (y - z)
上式中的Loss(简记为: L) 通常也被称作损失函数,它是衡量模型好坏的指标,在回归问题中均方误差是一种比较常见的形式,分类问题中通常会采用交叉熵损失函数,在后续的章节中会更详细的介绍。 对一个样本计算损失的代码实现如下:
1 Loss = (y1 - z)*(y1 - z) 2 print(y1) 3 print(z) 4 print(Loss)
[-0.00390539] [-0.63182506] [0.39428312]
因为计算损失时需要把每个样本的损失都考虑到,所以我们需要对单个样本的损失函数进行求和,并除以样本总数N。
L=1/N * Sigma(yi-zi)^2 (i=1 to 样本总数)
对上面的计算代码做出相应的调整,在Network类下面添加损失函数的计算过程如下
1 class Network(object): 2 def __init__(self, num_of_weights): 3 # 随机产生w的初始值 4 # 为了保持程序每次运行结果的一致性,此处设置固定的随机数种子 5 np.random.seed(0) 6 self.w = np.random.randn(num_of_weights, 1) 7 self.b = 0. 8 9 def forward(self, x): 10 z = np.dot(x, self.w) + self.b 11 return z 12 13 def loss(self, z, y): 14 error = z - y 15 cost = error * error 16 cost = np.mean(cost) 17 return cost
使用上面定义的Network类,可以方便的计算预测值和损失函数。 需要注意,类中的变量x, w,b, z, error等均是向量。以变量x为例,共有两个维度,一个代表特征数量(=13),一个代表样本数量(演示程序如下)。
1 net = Network(13) 2 # 此处可以一次性计算多个样本的预测值和损失函数 3 x1 = x[0:3] 4 y1 = y[0:3] 5 z = net.forward(x1) 6 print('predict: ', z) 7 loss = net.loss(z, y1) 8 print('loss:', loss)
predict: [[-0.63182506] [-0.55793096] [-1.00062009]] loss: 0.7229825055441156
此处演示了前三组数据(0,1,2)的预测值与损失值。x,y还是原来读取的原始数据:影响房价的因素,真实房价。
神经网络的训练
上述计算过程描述了如何构建神经网络,通过神经网络完成预测值和损失函数的计算。接下来将介绍如何求解参数w和b的数值,这个过程也称为模型训练。模型训练的目标是让定义的损失函数尽可能的小,也就是说找到一个参数解w和b使得损失函数取得极小值。
求解损失函数的极小值
基于最基本的微积分知识,函数在极值点处的导数为0。那么,让损失函数取极小值的w和b应该是下述方程组的解:
∂L/∂wj=0, for j=0,1,2...12
∂L/∂b=0
将样本数据(x,y)代入上面的方程组固然可以求解出w和b的值,但是这种方法只对线性回归这样简单的情况有效。如果模型中含有非线性变换,或者损失函数不是均方差这种简单形式,则很难通过上式求解。为了避免这一情况,下面我们将引入更加普适的数值求解方法。
梯度下降法
训练的关键是找到一组(w,b)使得损失函数L取极小值。我们先看一下损失函数L只随两个参数变化时的简单情形,启发下寻解的思路。
L=L(w5,w9)
这里我们将w0,w1,...,w12中除w5,w9之外的参数和b都固定下来,可以用图画出L(w5,w9)的形式。
1 net = Network(13) 2 losses = [] 3 #只画出参数w5和w9在区间[-160, 160]的曲线部分,已经包含损失函数的极值 4 w5 = np.arange(-160.0, 160.0, 1.0) 5 w9 = np.arange(-160.0, 160.0, 1.0) 6 losses = np.zeros([len(w5), len(w9)]) 7 8 #计算设定区域内每个参数取值所对应的Loss 9 for i in range(len(w5)): #对w5每间隔1遍历 10 for j in range(len(w9)): #对w9每间隔1遍历 11 net.w[5] = w5[i] 12 net.w[9] = w9[j] 13 z = net.forward(x) 14 loss = net.loss(z, y) 15 losses[i, j] = loss 16 17 #将两个变量和对应的Loss作3D图: 看来可以在程序的任意地方使用import 18 import matplotlib.pyplot as plt 19 from mpl_toolkits.mplot3d import Axes3D 20 fig = plt.figure('Loss') 21 ax = Axes3D(fig) 22 23 w5, w9 = np.meshgrid(w5, w9) 24 25 ax.plot_surface(w5, w9, losses, rstride=1, cstride=1, cmap='rainbow') 26 plt.show('Loss')

简单情形——只考虑两个参数w5和w9
对于这种简单情形,我们利用上面的程序在3维空间中画出了损失函数随参数变化的曲面图,从上图可以看出有些区域的函数值明显比周围的点小。需要说明的是:为什么这里我们选择w5和w9来画图?这是因为选择这两个参数的时候,可比较直观的从损失函数的曲面图上发现极值点的存在。其他参数组合,从图形上观测损失函数的极值点不够直观 (看来作者为了让我们看懂,不知试过多少次w的组合,才找到w5,w9这一对,真实煞费苦心)。
上文提到,直接求解导数方程的方式在多数情况下较困难,本质原因是导数方程往往正向求解容易(已知X,求得Y),反向求解较难(已知Y,求得X)。这种特性的方程在很多加密算法中较为常见,与日常见到的锁头特性一样:已知“钥匙”,锁头判断是否正确容易;已知“锁头”,反推钥匙的形状比较难。
这种情况特别类似于一位想从山峰走到坡谷的盲人,他看不见坡谷在哪(无法逆向求解出Loss导数为0时的参数值),但可以伸脚探索身边的坡度(当前点的导数值,也称为梯度)。那么,求解Loss函数最小值可以“从当前的参数取值,一步步的按照下坡的方向下降,直到走到最低点”实现。这种方法个人称它为“瞎子下坡法”。哦不,有个更正式的说法“梯度下降法”。
现在我们要找出一组[w5,w9]的值,使得损失函数最小,实现梯度下降法的方案如下:
- 随机的选一组初始值,例如: [w5,w9]=[−100.0,−100.0]
- 选取下一个点[w5′,w9′]使得 L(w5′,w9′)<L(w5,w9)
- 重复上面的步骤2,直到损失函数几乎不再下降
如何选择[w5′,w9′]是至关重要的,第一要保证L是下降的,第二要使得下降的趋势尽可能的快。微积分的基础知识告诉我们,沿着梯度的反方向,是函数值下降最快的方向,如下图所示在点P0(上图未画出),[w5,w9]=[−100.0,−100.0],梯度方向是图中P0点的箭头指向的方向,沿着箭头方向向前移动一小步,可以观察损失函数的变化。 在P0点,[w5,w9]=[−100.0,−100.0],
