神经网络基本原理简明教程之线性回归预测房价

1.1 提出问题
问题:在北京通州,距离通州区中心15公里的一套93平米的房子,大概是多少钱?

房价预测问题,成为了机器学习的一个入门话题,著名的波士顿的房价数据及相关的比赛已经很多了,但是美国的房子都是独栋的,前院后院停车库游泳池等等参数非常多,初学者可能理解起来有困难。我们不妨用简化版的北京通州的房价来举例,感受一下房价预测的过程。

影响北京通州房价的因素有很多,居住面积、地理位置、朝向、学区房、周边设施、建筑年份等等,其中,面积和地理位置是两个比较重要的因素。地理位置信息一般采用经纬度方式表示,但是经纬度是两个特征值,联合起来才有意义,因此,我们把它转换成了到通州区中心的距离。

我们有1000个样本,每个样本有两个特征值,一个标签值,示例如下:
在这里插入图片描述
特征值1 - 地理位置,统计得到:

最大值:21.96公里
最小值:2.02公里
平均值:12.13公里

特征值2 - 房屋面积,统计得到:

最大值:119平米
最小值:40平米
平均值:78.9平米

标签值 - 房价,单位为百万元:

最大值:674.37
最小值:181.38
平均值:420.64

这个数据是三维的,所以可以用两个特征值作为x和y,用标签值作为z,在xyz坐标中展示:
正向
在这里插入图片描述
侧向
在这里插入图片描述
从正向看,很像一块草坪,似乎是一个平面。再从侧向看,和第4章中的直线拟合数据很像。所以,对于这种三维的线性拟合,我们可以把它想象成为拟合一个平面,这个平面会位于这块“草坪”的中位,把“草坪”分割成上下两块更薄的“草坪”,最终使得所有样本点到这个平面的距离的平方和最小。

1.2 多元线性回归模型
由于表中可能没有恰好符合15公里、93平米条件的数据,因此我们需要根据1000个样本值来建立一个模型,来解决预测问题。

通过图示,我们基本可以确定这个问题是个线性回归问题,而且是典型的多元线性回归,即包括两个或两个以上自变量的回归。多元线性回归的函数模型如下:

y=a0+a1x1+a2x2+⋯+akxk
具体化到房价预测问题,上面的公式可以简化成:

z=x1⋅w1+x2⋅w2+b
抛开本例的房价问题,对于一般的应用问题,建立多元线性回归模型时,为了保证回归模型具有优良的解释能力和预测效果,应首先注意自变量的选择,其准则是:

1.自变量对因变量必须有显著的影响,并呈密切的线性相关;
2.自变量与因变量之间的线性相关必须是真实的,而不是形式上的;
3.自变量之间应具有一定的互斥性,即自变量之间的相关程度不应高于自变量与因变量之因的相关程度;
4.自变量应具有完整的统计数据,其预测值容易确定。

1.3 解决方案
如果用传统的数学方法解决这个问题,我们可以使用正规方程,从而可以得到数学解析解,然后再使用神经网络方式来求得近似解,从而比较两者的精度,再进一步调试神经网络的参数,达到学习的目的。

我们不妨先把两种方式在这里做一个对比,读者阅读并运行代码,得到结果后,再回到这里来仔细体会下面这个表格中的比较项:
在这里插入图片描述

2.1 正规方程解法
对于线性回归问题,除了前面提到的最小二乘法可以解决一元线性回归的问题外,也可以解决多元线性回归问题。

对于多元线性回归,可以用正规方程来解决,也就是得到一个数学上的解析解。它可以解决下面这个公式描述的问题:
在这里插入图片描述
2.2 简单的推导方法
在做函数拟合(回归)时,我们假设函数H为:
在这里插入图片描述
令b=w0,则:
在这里插入图片描述
公式3中的x是一个样本的n个特征值,如果我们把m个样本一起计算,将会得到下面这个矩阵:
在这里插入图片描述
公式5中的X和W的矩阵形状如下:
在这里插入图片描述然后我们期望假设函数的输出与真实值一致,则有:
在这里插入图片描述
其中,Y的形状如下:
在这里插入图片描述
直观上看,W = Y/X,但是这里三个值都是矩阵,而矩阵没有除法,所以需要得到X的逆矩阵,用Y乘以X的逆矩阵即可。但是又会遇到一个问题,只有方阵才有逆矩阵,而X不一定是方阵,所以要先把左侧变成方阵,就可能会有逆矩阵存在了。所以,先把等式两边同时乘以X的转置矩阵,以便得到X的方阵:
在这里插入图片描述
其中,XT是X的转置矩阵,XTX一定是个方阵,并且假设其存在逆矩阵,把它移到等式右侧来:’
在这里插入图片描述
至此可以求出W的正规方程。

2.3 复杂的推导方法
我们仍然使用均方差损失函数:
在这里插入图片描述
把b看作是一个恒等于1的feature,并把z=XW计算公式带入,并变成矩阵形式:
在这里插入图片描述
对w求导,令导数为0,就是W的最小值解:
在这里插入图片描述
求导后:

第一项的结果是:2XTXW
第二项和第三项的结果都是:XTY
第四项的结果是:0
再令导数为0:在这里插入图片描述
结论和公式10一样。

以上推导的基本公式可以参考第0章的公式60-69。

逆矩阵(XTX)−1可能不存在的原因是:

1.特征值冗余,比如x2=x21,即正方形的边长与面积的关系,不能做为两个特征同时存在
2.特征数量过多,比如特征数n比样本数m还要大

以上两点在我们这个具体的例子中都不存在。

2.4 代码实现
我们再看一下样本数据的样子:
在这里插入图片描述
根据公式(5),我们应该建立如下的X,Y矩阵:
在这里插入图片描述
根据公式(10):
在这里插入图片描述
1.X是1000x3的矩阵,X的转置是3x1000,XTX生成(3x3的矩阵
2.(XTX)−1也是3x3
3.再乘以XT,即(3x3)x(3x1000)的矩阵,变成3x1000
4.再乘以Y,Y是1000x1,所以(3x1000)x(1000x1)变成3x1,就是W的解,其中包括一个偏移值b和两个权重值w,3个值在一个向量里

资源包:HelperClass
在这里插入图片描述

import numpy as np

from HelperClass1.DataReader_1_1 import *

file_name = "ch05.npz"

if __name__ == '__main__':
    reader = DataReader_1_1(file_name)
    reader.ReadData()
    X,Y = reader.GetWholeTrainSamples()
    num_example = X.shape[0]
    one = np.ones((num_example,1))
    x = np.column_stack((one, (X[0:num_example,:])))

    a = np.dot(x.T, x)
    # need to convert to matrix, because np.linalg.inv only works on matrix instead of array
    b = np.asmatrix(a)
    c = np.linalg.inv(b)
    d = np.dot(c, x.T)
    e = np.dot(d, Y)
    print(e)
    b=e[0,0]
    w1=e[1,0]
    w2=e[2,0]
    print("w1=", w1)
    print("w2=", w2)
    print("b=", b)
    # inference
    z = w1 * 15 + w2 * 93 + b
    print("z=",z)

3.1 神经网络解法
与单特征值的线性回归问题类似,多变量(多特征值)的线性回归可以被看做是一种高维空间的线性拟合。以具有两个特征的情况为例,这种线性拟合不再是用直线去拟合点,而是用平面去拟合点。

3.2 定义神经网络结构
我们定义一个一层的神经网络,输入层为2或者更多,反正大于2了就没区别。这个一层的神经网络的特点是:
1.没有中间层,只有输入项和输出层(输入项不算做一层),
2.输出层只有一个神经元,
3.神经元有一个线性输出,不经过激活函数处理,即在下图中,经过Σ求和得到Z值之后,直接把Z值输出。
在这里插入图片描述
输入层
单独看第一个样本是这样的:在这里插入图片描述
一共有1000个样本,每个样本2个特征值,X就是一个1000×2的矩阵:
在这里插入图片描述
x1 表示第一个样本,x1,1表示第一个样本的一个特征值,y1是第一个样本的标签值。

权重W和B
由于我们只想完成一个回归(拟合)任务,所以输出层只有一个神经元。由于是线性的,所以没有用激活函数。
在这里插入图片描述
写成矩阵形式:
在这里插入图片描述
上述公式中括号中的数字表示该矩阵的(行x列)数。

对于拟合,可以想象成用一支笔在一堆点中画一条直线或者曲线,而那一个神经元就是这支笔。如果有多个神经元,可以画出多条线来,就不是拟合了,而是分类。

损失函数
因为是线性回归问题,所以损失函数使用均方差函数。
在这里插入图片描述
其中,zi是样本预测值,yi是样本的标签值。

3.3 反向传播
单样本多特征计算
与上一章不同,本章中的前向计算是多特征值的公式:
在这里插入图片描述
因为x有两个特征值,对应的W也有两个权重值。xi1表示第i个样本的第1个特征值,所以无论是x还是w都是一个向量或者矩阵了,那么我们在反向传播方法中的梯度计算公式还有效吗?答案是肯定的,我们来一起做个简单推导。

由于W被分成了w1和w2两部分,根据公式1和公式2,我们单独对它们求导:

在这里插入图片描述
求损失函数对W矩阵的偏导,是无法求的,所以要变成求各个W的分量的偏导。由于W的形状是:
在这里插入图片描述
所以求loss对W的偏导,由于W是个矩阵,所以应该这样写:
在这里插入图片描述
多样本多特征计算
当进行多样本计算时,我们用m=3个样本做一个实例化推导:
在这里插入图片描述
在这里插入图片描述
3.4 代码实现
我们依然采用第四章中已经写好的HelperClass目录中的那些类,来表示我们的神经网络。虽然此次神经元多了一个输入,但是不用改代码就可以适应这种变化,因为在前向计算代码中,使用的是矩阵乘的方式,可以自动适应x的多个列的输入,只要对应的w的矩阵形状是正确的即可。

但是在初始化时,我们必须手动指定x和w的形状,如下面的代码所示:

from HelperClass.SimpleDataReader import *

if __name__ == '__main__':
    # data
    reader = SimpleDataReader()
    reader.ReadData()
    # net
    params = HyperParameters(2, 1, eta=0.1, max_epoch=100, batch_size=1, eps = 1e-5)
    net = NeuralNet(params)
    net.train(reader)
    # inference
    x1 = 15
    x2 = 93
    x = np.array([x1,x2]).reshape(1,2)
    print(net.inference(x))

在参数中,指定了学习率0.1,最大循环次数100轮,批大小1个样本,以及停止条件损失函数值1e-5。

在神经网络初始化时,指定了input_size=2,且output_size=1,即一个神经元可以接收两个输入,最后是一个输出。

最后的inference部分,是把两个条件(15公里,93平方米)代入,查看输出结果。

在下面的神经网络的初始化代码中,W的初始化是根据input_size和output_size的值进行的。

class NeuralNet(object):
    def __init__(self, params):
        self.params = params
        self.W = np.zeros((self.params.input_size, self.params.output_size))
        self.B = np.zeros((1, self.params.output_size))

正向计算的代码

class NeuralNet(object):
    def __forwardBatch(self, batch_x):
        Z = np.dot(batch_x, self.W) + self.B
        return Z

误差反向传播的代码

class NeuralNet(object):
    def __backwardBatch(self, batch_x, batch_y, batch_z):
        m = batch_x.shape[0]
        dZ = batch_z - batch_y
        dB = dZ.sum(axis=0, keepdims=True)/m
        dW = np.dot(batch_x.T, dZ)/m
        return dW, dB

3.5 运行结果
在Visual Studio 2017中,可以使用Ctrl+F5运行Level2的代码,但是,会遇到一个令人沮丧的打印输出:

epoch=0
NeuralNet.py:32: RuntimeWarning: invalid value encountered in subtract
  self.W = self.W - self.params.eta * dW
0 500 nan
epoch=1
1 500 nan
epoch=2
2 500 nan
epoch=3
3 500 nan
......

减法怎么会出问题?什么是nan?

nan的意思是数值异常,导致计算溢出了,出现了没有意义的数值。现在是每500个迭代监控一次,我们把监控频率调小一些,再试试看:

epoch=0
0 10 6.838664338516814e+66
0 20 2.665505502247752e+123
0 30 1.4244204612680962e+179
0 40 1.393993758296751e+237
0 50 2.997958629609441e+290
NeuralNet.py:76: RuntimeWarning: overflow encountered in square
  LOSS = (Z - Y)**2
0 60 inf
0 70 inf
0 80 inf
0 90 inf
0 100 inf
0 110 inf
NeuralNet.py:32: RuntimeWarning: invalid value encountered in subtract
  self.W = self.W - self.params.eta * dW
0 120 nan
0 130 nan

前10次迭代,损失函数值已经达到了6.83e+66,而且越往后运行值越大,最后终于溢出了。下面的损失函数历史记录也表明了这一过程。

在这里插入图片描述
3.6 寻找失败的原因
我们可以在NeuralNet.py文件中,在下述代码行上设置断点,跟踪一下训练过程,以便找到问题所在:
在这里插入图片描述
在VS2017中用F5运行debug模式,看第50行的结果:

batch_x
array([[ 4.96071728, 41.        ]])
batch_y
array([[244.07856544]])

返回的样本数据是正常的。再看下一行:

batch_z
array([[0.]])

第一次运行前向计算,由于W和B初始值都是0,所以z也是0,这是正常的。再看下一行:

dW
array([[ -1210.80475712],
       [-10007.22118309]])
dB
array([[-244.07856544]])

dW和dB的值都非常大,这是因为下面这行代码:

在这里插入图片描述
batch_z是0,batch_y是244.078,二者相减,是-244.078,因此dB就是-244.078,dW因为矩阵乘了batch_x,值就更大了。

再看W和B的更新值,一样很大:

self.W
array([[ 121.08047571],
       [1000.72211831]])
self.B
array([[24.40785654]])

如果W和B的值很大,那么再下一轮进行前向计算时,会得到更糟糕的结果:

batch_z
array([[82459.53752331]])

果不其然,这次的z值飙升到了8万多,如此下去,几轮以后数值溢出是显而易见的事情了。

那么我们到底遇到了什么情况?

4.1 样本特征数据归一化
发现问题的根源

仔细分析一下屏幕打印信息,前两次迭代的损失值已经是天文数字了,后面的W和B的值也在不断变大,说明网络发散了。难度我们遇到了传说中的梯度爆炸!数值太大,导致计算溢出了。第一次遇到这个情况,但相信不会是最后一次,因为这种情况在神经网络中太常见了。

回想一个问题:为什么在第4章中,我们没有遇到这种情况?把第4章的数据样本拿来看一看:
在这里插入图片描述
所有的X值(服务器数量除以1000后的值)都是在[0,1]之间的,而本章中的房价数据有两个特征值,一个是公里数,一个是平米数,全都是不是在[0,1]之间的,并且取值范围还不相同。我们不妨把本次样本数据也做一下这样的处理,亦即“归一化”。

其实,数据归一化是深度学习的必要步骤之一,已经是大师们的必杀技能,也因此它很少被各种博客/文章所提及,以至于初学者们经常被坑。

根据5.0.1中对数据的初步统计,我们是不是也可以把公里数都除以100,而平米数都除以1000呢,这样也会得到[0,1]之间的数字?公里数的取值范围是[2,22],除以100后变成了[0.02,0.22]。平米数的取值范围是[40,120],除以1000后变成了[0.04,0.12]。

对本例来说这样做肯定是可以正常工作的,但是下面我们要介绍一种更科学合理的做法。

4.2 为什么要做归一化
理论层面上,神经网络是以样本在事件中的统计分布概率为基础进行训练和预测的,所以它对样本数据的要求比较苛刻。具体说明如下:

1.样本的各个特征的取值要符合概率分布,即[0,1]

2.样本的度量单位要相同。我们并没有办法去比较1米和1公斤的区别,但是,如果我们知道了1米在整个样本中的大小比例,以及1公斤在整个样本中的大小比例,比如一个处于0.2的比例位置,另一个处于0.3的比例位置,就可以说这个样本的1米比1公斤要小!

3.神经网络假设所有的输入输出数据都是标准差为1,均值为0,包括权重值的初始化,激活函数的选择,以及优化算法的的设计。

4.数值问题
归一化可以避免一些不必要的数值问题。因为激活函数sigmoid/tanh的非线性区间大约在[-1.7,1.7]。意味着要使神经元有效,线性计算输出的值的数量级应该在1(1.7所在的数量级)左右。这时如果输入较大,就意味着权值必须较小,一个较大,一个较小,两者相乘,就引起数值问题了。

5.梯度更新
若果输出层的数量级很大,会引起损失函数的数量级很大,这样做反向传播时的梯度也就很大,这时会给梯度的更新带来数值问题。

6.学习率
知道梯度非常大,学习率就必须非常小,因此,学习率(学习率初始值)的选择需要参考输入的范围,不如直接将数据归一化,这样学习率就不必再根据数据范围作调整。 对w1适合的学习率,可能相对于w2来说会太小,若果使用适合w1的学习率,会导致在w2方向上步进非常慢,会消耗非常多的时间,而使用适合w2的学习率,对w1来说又太大,搜索不到适合w1的解。

4.3 从损失函数等高线图分析归一化的必要性
在房价数据中,地理位置的取值范围是[2,20],而房屋面积的取值范围为[40,120],二者相差太远,根本不可以放在一起计算了?

根据公式z=x1w1+x2w2+b,神经网络想学习w1和w2,但是数值范围问题导致神经网络来说很难“理解”。下图展示了归一化前后的情况Loss值的等高图,意思是地理位置和房屋面积取不同的值时,作为组合来计算损失函数值时,形成的类似地图的等高图。左侧为归一化前,右侧为归一化后:
在这里插入图片描述
在这里插入图片描述
房屋面积的取值范围是[40,120],而地理位置的取值范围是[2,20],二者会形成一个很扁的椭圆,如左侧。这样在寻找最优解的时候,过程会非常曲折。运气不好的话,根本就没法训练。

4.4 归一化的基本概念
有三个类似的概念,归一化,标准化,中心化。

归一化

把数据线性地变成[0,1]或[-1,1]之间的小数,把带单位的数据(比如米,公斤)变成无量纲的数据,区间缩放。

归一化有三种方法:

1.Min-Max归一化:
在这里插入图片描述
2.平均值归一化
在这里插入图片描述
3.非线性归一化
对数转换:
在这里插入图片描述
反余切转换:
在这里插入图片描述
标准化
把每个特征值中的所有数据,变成平均值为0,标准差为1的数据,最后为正态分布。Z-score规范化(标准差标准化 / 零均值标准化,其中std是标准差):在这里插入图片描述
中心化
平均值为0,无标准差要求:
在这里插入图片描述
4.5 如何做数据归一化
我们再看看样本的数据:
在这里插入图片描述
按照归一化的定义,我们只要把地理位置列和居住面积列分别做归一化就达到要求了,即:
在这里插入图片描述
注意:
1.我们并没有归一化样本的标签Y数据,所以最后一行的价格还是保持不变
2.我们是对两列特征值分别做归一化处理的

4.6 代码实现
在HelperClass目录的SimpleDataReader.py文件中,给该类增加一个方法:

def NormalizeX(self):
    X_new = np.zeros(self.XRaw.shape)
    num_feature = self.XRaw.shape[1]
    self.X_norm = np.zeros((2,num_feature))
    # 按列归一化,即所有样本的同一特征值分别做归一化
    for i in range(num_feature):
        # get one feature from all examples
        col_i = self.XRaw[:,i]
        max_value = np.max(col_i)
        min_value = np.min(col_i)
        # min value
        self.X_norm[0,i] = min_value 
        # range value
        self.X_norm[1,i] = max_value - min_value 
        new_col = (col_i - self.X_norm[0,i])/(self.X_norm[1,i])
        X_new[:,i] = new_col
    #end for
    self.XTrain = X_new

返回值X_new是归一化后的样本,和原始数据的形状一样。

再把主程序修改一下,在ReadData()方法后,紧接着调用NormalizeX()方法:

if __name__ == '__main__':
    # data
    reader = SimpleDataReader()
    reader.ReadData()
    reader.NormalizeX()
    # net
    params = HyperParameters(eta=0.1, max_epoch=10, batch_size=1, eps = 1e-5)
    net = NeuralNet(params, 2, 1)
    net.train(reader)

4.7 运行结果
运行上述代码,看打印结果:

epoch=9
9 0 391.75978721600353
9 100 387.79811202735783
9 200 502.9576560855685
9 300 395.63883403610765
9 400 417.61092908059885
9 500 404.62859838907883
9 600 398.0285538622818
9 700 469.12489440138637
9 800 380.78054509441193
9 900 575.5617634691969
W= [[-41.71417524]
 [395.84701164]]
B= [[242.15205099]]
z= [[37366.53336103]]

虽然损失函数值没有像我们想象的那样趋近于0,但是却稳定在了400左右震荡,这也算是收敛!看一下损失函数图像:在这里插入图片描述

再看看W和B的输出值和z的预测值:

w1 = -41.71417524
w2 = 395.84701164
b = 242.15205099
z = 37366.53336103

回忆一下正规方程的输出值:

w1= -2.0184092853092226
w2= 5.055333475112755
b= 46.235258613837644
z= 486.1051325196855

正规方程预测房价结果:

Z=−2.018×15+5.055×93+46.235=486.105(万元)

神经网络预测房价结果:

Z=−14.714×15+395.847×93+242.152=37366(万元)

好吧,我们遇到了天价房!这是怎么回事儿?难道和我们做数据归一化有关系?

在5.0.1中,我们想象神经网络会寻找一个平面,来拟合这些空间中的样本点,是不是这样呢?我们通过下面的函数来实现这个可视化:

def ShowResult(net, reader):
    X,Y = reader.GetWholeTrainSamples()
    fig = plt.figure()
    ax = Axes3D(fig)
    ax.scatter(X[:,0],X[:,1],Y)

    p = np.linspace(0,1)
    q = np.linspace(0,1)
    P,Q = np.meshgrid(p,q)
    R = np.hstack((P.ravel().reshape(2500,1), Q.ravel().reshape(2500,1)))
    Z = net.inference(R)
    Z = Z.reshape(50,50)
    ax.plot_surface(P,Q,Z, cmap='rainbow')

前半部分代码先是把所有的点显示在三维空间中,我们曾经描述它们像一块厚厚的草坪。后半部分的代码在[0,1]空间内形成了一个50x50的网格,亦即有2500个点,这些点都是有横纵坐标的。然后把这些点送入神经网络中做预测,得到了2500个Z值,相当于第三维的坐标值。最后把这2500个三维空间的点,以网格状显示在空间中,就形成了下面的可视化的结果:

正向
在这里插入图片描述
侧向
在这里插入图片描述
从正向图可以看到,真的形成了一个平面;从侧向图可以看到,这个平面也确实穿过了那些点,并且把它们分成了上下两个部分。只不过由于训练精度的问题,没有做到平分,而是斜着穿过了点的区域,就好像第4章中的那个精度不够的线性回归的结果。

细心的读者可能会问两个问题:

1.为什么要在[0,1]空间中形成50x50的网格呢?
2.50这个数字从哪里来的?

NumPy库的np.linspace(0,1)的含义,就是在[0,1]空间中生成50个等距的点,第三个参数不指定时,缺省是50。因为我们前面对样本数据做过归一化,统一到了[0,1]空间中,这就方便了我们对问题的分析,不用考虑每个特征值的实际范围是多大了。

这下子我们可以大致放心了,神经网络的训练结果并没有错,一定是别的地方出了什么问题。在下一节中我们来一起看看问题出在哪里!

5.1 归一化的后遗症
对比结果
在上一节中,我们使用了如下超参进行神经网络的训练:

params = HyperParameters(eta=0.1, max_epoch=10, batch_size=1, eps = 1e-5)

我们再把每次checkpoint的W和B的值打印出来:

9 0 437.5399553941636 [[-35.46926435] [399.01136072]] [[252.69305588]]
9 100 420.78580862641473 [[-36.93198181] [400.03047293]] [[251.26503706]]
9 200 398.58439997901917 [[-39.90602892] [390.9923031 ]] [[253.77229392]]
9 300 393.4058623386585 [[-31.26023019] [389.38500924]] [[247.81021777]]
9 400 380.95014666219294 [[-41.71204444] [400.49621558]] [[243.90381925]]
9 500 402.3345372333071 [[-50.16424871] [400.57038807]] [[242.88921572]]
9 600 419.2032196399209 [[-38.64935779] [397.40267036]] [[235.76347754]]
9 700 388.91219270279 [[-41.87540883] [406.51486971]] [[245.11439119]]
9 800 387.30767281965444 [[-40.57188118] [407.41384495]] [[237.77896547]]
9 900 413.7210407763991 [[-36.67601742] [406.55322285]] [[246.8067483]]

打印结果中每列的含义:
1.epoch
2.iteration
3.loss
4.w1
5.w2
6.b
可以看到loss值、w1、w2、b的值,每次跳跃都很大,怀疑是学习率过高导致的梯度下降在最优解附近徘徊。所以,我们先把超参修改一下:

params = HyperParameters(eta=0.01, max_epoch=50, batch_size=10, eps=1e-5)

做了三处修改:

1.学习率缩小10倍,变成0.01
2.max_epoch扩大50倍,让网络得到充分训练
3.batch_size=10,使用mini-batch批量样本训练,提高精度,减缓个别样本引起的跳跃程度

运行结果:

499 9 380.9733976063486 [[-40.0502582 ] [399.59874166]] [[245.01472597]]
499 19 380.91972396603296 [[-39.96834496] [399.55957677]] [[244.92705677]]
499 29 380.6255377532388 [[-40.31047769] [399.26167586]] [[244.19126217]]
499 39 380.6057213728372 [[-40.2563536 ] [399.35785505]] [[244.53062721]]
499 49 380.657163633654 [[-40.16087354] [399.36180641]] [[244.67728494]]
499 59 380.59442069555746 [[-40.32063337] [399.48881984]] [[244.37834746]]
499 69 380.92999531800933 [[-40.57175379] [399.16255261]] [[243.81211148]]
499 79 380.687742276159 [[-40.4266247 ] [399.30514719]] [[244.0496554]]
499 89 380.62299460835936 [[-40.2782923 ] [399.34224968]] [[244.14309928]]
499 99 380.5935045560184 [[-40.26440193] [399.39472352]] [[244.3928586]]

可以看到达到了我们的目的,loss、w1、w2、b的值都很稳定。我们使用这批结果做为分析基础。首先列出W和B的训练结果:
在这里插入图片描述
再列出归一化后的数据:
在这里插入图片描述
通过对比我发现,关于W的结果,第一张表最后一行的数据,和第二张表最后一行的数据,有惊人的相似之处!这是为什么呢?

5.2 还原真实的W,B值
我们唯一修改的地方,就是样本数据特征值的归一化,我们并没有修改标签值。可以大概猜到W的值和样本特征值的缩放有关系,而且缩放倍数非常相似,甚至可以说一致。下面推导一下这种现象的数学基础。

假设在归一化之前,真实的样本值是X,真实的权重值是W;在归一化之后,样本值变成了X′,训练出来的权重值是W′:
在这里插入图片描述
由于训练时标签值(房价)并没有做归一化,意味着我们是用真实的房价做的训练,所以预测值和标签值应该相等,所以:
在这里插入图片描述
归一化的公式是:
在这里插入图片描述
为了简化书写,我们令xm=xmax−xmin,把公式2代入公式1:
在这里插入图片描述
公式3中,x1,x2是变量,其它都是常量,如果想让公式3等式成立,则变量项和常数项分别相等,即:
在这里插入图片描述
下面我们用实际数值代入公式4,5,6:
在这里插入图片描述
可以看到公式7、8、9的计算结果(神经网络的训练结果的变换值)与正规方程的计算结果(-2.018, 5.055, 46.235)基本相同,基于神经网络是一种近似解的考虑,可以认为这种推导是有理论基础和试验证明的。

5.3 代码实现
下面的代码实现了公式4,5,6:

# get real weights and bias
def DeNormalizeWeightsBias(net, dataReader):
    W_real = np.zeros_like(net.W)
    X_Norm = dataReader.GetDataRange()
    for i in range(W_real.shape[0]):
        W_real[i,0] = net.W[i,0] / X_Norm[i,1]

    B_real = net.B - W_real[0,0]*X_Norm[0,0] - W_real[1,0]*X_Norm[1,0]
    return W_real, B_real

X_Norm是我们在做归一化时保留下来的样本的两个特征向量的最小值和数值范围(最大值减去最小值)。

修改主程序如下:if __name__ == '__main__':
    # data
    reader = SimpleDataReader()
    reader.ReadData()
    reader.NormalizeX()
    # net
    params = HyperParameters(eta=0.01, max_epoch=500, batch_size=10, eps = 1e-5)
    net = NeuralNet(params, 2, 1)
    net.train(reader, checkpoint=0.1)
    # inference
    W_real, B_real = DeNormalizeWeightsBias(net, reader)
    print("W_real=", W_real)
    print("B_real=", B_real)

    x1 = 15
    x2 = 93
    x = np.array([x1,x2]).reshape(1,2)
    z = np.dot(x, W_real) + B_real
    print("Z=", z)

    ShowResult(net, reader)

在net.train()方法返回之后,训练好的W和B的值就保存在NeuralNet类的属性里了。然后通过调用DeNormalizeWeightsBias()函数,把它们转换成真实的W_real和B_real值,就好比我们不做归一化而能训练出来的权重值一样。

最后在推理预测时,我们直接使用了np.dot()公式,而没有使用net.inference()方法,是因为在net实例中的W和B是还原前的值,做前向计算时还是会有问题,所以我们直接把前向计算公式拿出来,代入W_real和B_real,就可以得到真实的预测值了。

5.4 运行结果
运行上述代码,观察最后部分的打印输出:

499 89 380.5945851413049 [[-40.25737641]
 [399.46151506]] [[244.26562146]]
499 99 380.5934686827507 [[-40.23261123]
 [399.36389489]] [[244.39118797]]
W= [[-40.23261123]
 [399.36389489]]
B= [[244.39118797]]
W_real= [[-2.01737219]
 [ 5.05523918]]
B_real= [[46.26647363]]
Z= [[486.14313417]]

把结果与正规方程的结果对比一下:在这里插入图片描述
二者几乎一样,可以认为我们成功了!但是这一套代码下来,总觉得有些啰嗦,对于简单的线性问题来说,这么做可以,如果遇到非线性问题,或者深层网络,这么做是不是也可以呢?

正确的推理预测方法
6.1 预测数据的归一化
在上一节中,我们在用训练出来的模型预测房屋价格之前,还需要先还原W和B的值,这看上去比较麻烦,下面我们来介绍一种正确的推理方法。

既然我们在训练时可以把样本数据归一化,那么在预测时,把预测数据也做相同方式的归一化,不是就可以和训练数据一样进行预测了吗?且慢!这里有一个问题,训练时的样本数据是批量的,至少是成百成千的数量级。但是预测时,一般只有一个或几个数据,如何做归一化?

我们在针对训练数据做归一化时,得到的最重要的数据是训练数据的最小值和最大值,我们只需要把这两个值记录下来,在预测时使用它们对预测数据做归一化,这就相当于把预测数据“混入”训练数据。前提是预测数据的特征值不能超出训练数据的特征值范围,否则有可能影响准确程度。

6.2 代码实现
基于这种想法,我们先给SimpleDataReader类增加一个方法NormalizePredicateData(),如下述代码:

class SimpleDataReader(object):
    # normalize data by self range and min_value
    def NormalizePredicateData(self, X_raw):
        X_new = np.zeros(X_raw.shape)
        n = X_raw.shape[1]
        for i in range(n):
            col_i = X_raw[:,i]
            X_new[:,i] = (col_i - self.X_norm[i,0]) / self.X_norm[i,1]
        return X_new

X_norm数组中的数据,是在训练时从样本数据中得到的最大值最小值,比如:
在这里插入图片描述
所以,最后X_new就是按照训练样本的规格归一化好的预测归一化数据,然后我们把这个预测归一化数据放入网络中进行预测:

import numpy as np
from HelperClass.NeuralNet import *

if __name__ == '__main__':
    # data
    reader = SimpleDataReader()
    reader.ReadData()
    reader.NormalizeX()
    # net
    params = HyperParameters(eta=0.01, max_epoch=100, batch_size=10, eps = 1e-5)
    net = NeuralNet(params, 2, 1)
    net.train(reader, checkpoint=0.1)
    # inference
    x1 = 15
    x2 = 93
    x = np.array([x1,x2]).reshape(1,2)
    x_new = reader.NormalizePredicateData(x)
    z = net.inference(x_new)
    print("Z=", z)

6.3 运行结果

......
199 69 380.66017104568533 [[-40.46214107]
 [399.22941114]] [[244.17767124]]
199 79 380.74980617596043 [[-40.54801022]
 [399.27413915]] [[244.00581217]]
199 89 380.5933565144328 [[-40.24324555]
 [399.35384485]] [[244.398389]]
199 99 380.5942402877278 [[-40.23494571]
 [399.40443921]] [[244.388824]]
W= [[-40.23494571]
 [399.40443921]]
B= [[244.388824]]
Z= [[486.16645199]]

比较一下正规方程的结果:

z= 486.1051325196855

二者非常接近,可以说这种方法的确很方便,把预测数据看作训练数据的一个记录,先做归一化,再做预测,这样就不需要把权重矩阵还原了。

看上去我们已经完美地解决了这个问题,但是且慢,仔细看看loss值,还有w和b的值,都是几十几百的数量级,这和神经网络的概率计算的优点并不吻合,实际上它们的值都应该在[0,1]之间的。

大数量级的数据有另外一个问题,就是它的波动有可能很大。目前我们还没有使用激活函数,一旦网络复杂了,开始使用激活函数时,像486.166这种数据,一旦经过激活函数就会发生梯度饱和的现象,输出值总为1,这样对于后面的网络就没什么意义了,因为输入值都是1。

好吧,看起来问题解决得并不完美,我们看看还能有什么更好的解决方案!

对标签值归一化
7.1发现问题
这一节里我们重点解决在训练过程中的数值的数量级的问题。

我们既然已经对样本数据特征值做了归一化,那么如此大数值的损失函数值是怎么来的呢?看一看损失函数定义:在这里插入图片描述
其中,zi是预测值,yi是标签值。初始状态时,W和B都是0,所以,经过前向计算函数Z=X⋅W+B的结果是0,但是Y值很大,处于[181.38, 674.37]之间,再经过平方计算后,一下子就成为至少5位数的数值了。

再看反向传播时的过程:

   def __backwardBatch(self, batch_x, batch_y, batch_z):
        m = batch_x.shape[0]
        dZ = batch_z - batch_y
        dB = dZ.sum(axis=0, keepdims=True)/m
        dW = np.dot(batch_x.T, dZ)/m
        return dW, dB

第二行代码求得的dZ,与房价是同一数量级的,这样经过反向传播后,dW和dB的值也会很大,导致整个反向传播链的数值都很大。我们可以debug一下,得到第一反向传播时的数值是:

dW
array([[-142.59982906],
       [-283.62409678]])
dB
array([[-443.04543906]])
上述数值又可能在读者的机器上是不一样的,因为样本做了shuffle,但是不影响我们对问题的分析。

这么大的数值,需要把学习率设置得很小,比如0.001,才可以落到[0,1]区间,但是损失函数值还是不能变得很小。

如果我们像对特征值做归一化一样,把标签值也归一化到[0,1]之间,是不是有帮助呢?

7.2 代码实现
参照X的归一化方法,对Y的归一化公式如下:
在这里插入图片描述

在SimpleDataReader类中增加新方法如下class

SimpleDataReader(object):
    def NormalizeY(self):
        self.Y_norm = np.zeros((1,2))
        max_value = np.max(self.YRaw)
        min_value = np.min(self.YRaw)
        # min value
        self.Y_norm[0, 0] = min_value 
        # range value
        self.Y_norm[0, 1] = max_value - min_value 
        y_new = (self.YRaw - min_value) / self.Y_norm[0, 1]
        self.YTrain = y_new:

原始数据中,Y的数值范围是:

最大值:674.37
最小值:181.38
平均值:420.64

归一化后,Y的数值范围是:

最大值:1.0
最小值:0.0
平均值:0.485

注意,我们同样记住了Y_norm的值便于以后使用。

修改主程序代码,增加对Y归一化的方法调用NormalizeY():

# main
if __name__ == '__main__':
    # data
    reader = SimpleDataReader()
    reader.ReadData()
    reader.NormalizeX()
    reader.NormalizeY()
    # net
    params = HyperParameters(eta=0.01, max_epoch=200, batch_size=10, eps=1e-5)
    net = NeuralNet(params, 2, 1)
    net.train(reader, checkpoint=0.1)
    # inference
    x1 = 15
    x2 = 93
    x = np.array([x1,x2]).reshape(1,2)
    x_new = reader.NormalizePredicateData(x)
    z = net.inference(x_new)
    print("z=", z)

7.3 运行结果
运行上述代码得到的结果其实并不令人满意:

......
199 79 0.0015661482894344493 [[-0.08155304]
 [ 0.81028239]] [[0.12820503]]
199 89 0.001566005355499641 [[-0.08173551]
 [ 0.80999893]] [[0.1275476]]
199 99 0.0015663978030319194 [[-0.08194777]
 [ 0.80973365]] [[0.12714971]]
W= [[-0.08194777]
 [ 0.80973365]]
B= [[0.12714971]]
z= [[0.61707273]]

虽然W和B的值都已经处于[-1,1]之间了,但是z的值也在[0,1]之间,一套房子不可能卖0.61万元!

聪明的读者可能会想到:既然对标签值做了归一化,那么我们在得到预测结果后,需要对这个结果应该做反归一化。

根据公式2,反归一化的公式应该是:
在这里插入图片描述
代码如下:

if __name__ == '__main__':
    # data
    reader = SimpleDataReader()
    reader.ReadData()
    reader.NormalizeX()
    reader.NormalizeY()
    # net
    params = HyperParameters(eta=0.01, max_epoch=200, batch_size=10, eps=1e-5)
    net = NeuralNet(params, 2, 1)
    net.train(reader, checkpoint=0.1)
    # inference
    x1 = 15
    x2 = 93
    x = np.array([x1,x2]).reshape(1,2)
    x_new = reader.NormalizePredicateData(x)
    z = net.inference(x_new)
    print("z=", z)
    Z_real = z * reader.Y_norm[0,1] + reader.Y_norm[0,0]
    print("Z_real=", Z_real)

倒数第二行代码,就是公式3。运行…结果如下:

W= [[-0.08149004]
 [ 0.81022449]]
B= [[0.12801985]]
z= [[0.61856996]]
Z_real= [[486.33591769]]

看Z_real的值,完全满足要求!

总结一下从本章中学到的正确的方法:

1.X必须归一化,否则无法训练;
2.Y值不在[0,1]之间时,要做归一化,好处是迭代次数少;
3.如果Y做了归一化,对得出来的预测结果做关于Y的反归一化
至此,我们完美地解决了北京通州地区的房价预测问题!但是还没有解决自己可以有能力买一套北京通州的房子的问题…

完整代码:
1.ch5.npz是训练集:可以用自己的训练集
2.扫描二维码获取HelperClass包,封装了一些函数
在这里插入图片描述
3.主函数代码

import numpy as np

from HelperClass.NeuralNet_1_1 import *

file_name = "ch05.npz"

# main
if __name__ == '__main__':
    # data
    # 找到文件路劲
    reader = DataReader_1_1(file_name)
    # 读取出需要的数据
    reader.ReadData()
    # 归一化X和Y
    reader.NormalizeX()
    reader.NormalizeY()
    # net
    # 设定训练的一些条件
    hp = HyperParameters_1_0(2, 1, eta=0.01, max_epoch=200, batch_size=10, eps=1e-5)
    # 放入神经网络中
    net = NeuralNet_1_1(hp)
    # 训练数据
    net.train(reader, checkpoint=0.1)
    # inference
    x1 = 15
    x2 = 93
    # 待预测的值
    x = np.array([x1, x2]).reshape(1, 2)
    # 将测试值也放入训练集中归一化
    x_new = reader.NormalizePredicateData(x)
    # 测试出最终的房价
    z = net.inference(x_new)
    # 打印出归一化后的结果
    print("z=", z)
    # 反归一化  利用记录下来的数据
    Z_true = z * reader.Y_norm[0, 1] + reader.Y_norm[0, 0]
    # 打印出最终预测的房价
    print("Z_true=", Z_true)
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值