神经网络基本原理简明教程之非线性回归

本博客代码所需的包和数据集:链接: https://pan.baidu.com/s/1n-WiIzyMep2qRWDfRc8sYg 提取码: 4keg

非线性回归

从这一步开始进入了两层神经网络的学习,从而解决非线性问题。

在两层神经网络之间,必须有激活函数连接,从而加入非线性因素,提高神经网络的能力。所以,我们先从激活函数学起,一类是挤压型的激活函数,常用于简单网络的学习;另一类是半线性的激活函数,常用于深度网络的学习。

接下来我们将验证著名的万能近似定理,建立一个双层的神经网络,来拟合一个比较复杂的函数。

在上面的双层神经网络中,已经出现了很多的超参,都会影响到神经网络的训练结果。所以在完成了基本的拟合任务之后,我们将会尝试着调试这些参数,得到更好的训练效果(又快又好),从而得到超参调试的第一手经验。

激活函数

1 激活函数的基本作用

看神经网络中的一个神经元,为了简化,假设该神经元接受三个输入,分别为x1,x2,x3,那么:

在这里插入图片描述
在这里插入图片描述
激活函数也就是a=σ(z)这一步了,他有什么作用呢?

1 给神经网络增加非线性因素

2 把公式1的计算结果压缩到[0,1]之间,便于后面的计算。

激活函数的基本性质:

1 非线性:线性的激活函数和没有激活函数一样

2可导性:做误差反向传播和梯度下降,必须要保证激活函数的可导性

3 单调性:单一的输入会得到单一的输出,较大值的输入得到较大值的输出

在物理试验中使用的继电器,是最初的激活函数的原型:当输入电流大于一个阈值时,会产生足够的磁场,从而打开下一级电源通道,如下图所示:

在这里插入图片描述
用到神经网络中的概念,用‘1’来代表一个神经元被激活,‘0’代表一个神经元未被激活。

这个Step函数有什么不好的地方呢?主要的一点就是,他的梯度(导数)恒为零(个别点除外)。反向传播公式中,梯度传递用到了链式法则,如果在这样一个连乘的式子其中有一项是零,这样的梯度就会恒为零,是没有办法进行反向传播的。

2 何时会用到激活函数

激活函数用在神经网络的层与层之间的连接,神经网络的最后一层不用激活函数。

神经网络不管有多少层,最后的输出层决定了这个神经网络能干什么。在单层神经网络中,我们学习到了以下示例:

在这里插入图片描述
从上表可以看到,我们一直没有使用激活函数,而只使用了分类函数。对于多层神经网络也是如此,在最后一层只会用到分类函数来完成二分类或多分类任务,如果是拟合任务,则不需要分类函数。

很多文字材料中通常把激活函数和分类函数混淆在一起说,原因其实只有一个:在二分类任务中使用的Logistic分类函数与在神经网络之间连接的Sigmoid激活函数,是同样的形式。所以它既是激活函数,又是分类函数,是个特例。

简言之:

1 神经网络最后一层不需要激活函数

2 激活函数只用于连接前后两层神经网络

挤压型激活函数 Squashing Function

又可以叫饱和型激活函数,因为在输入值域的绝对值较大的时候,它的输出在两端是饱和的。挤压型激活函数中,用的最多的是Sigmoid函数,所谓Sigmoid函数,原意是指一类函数,它们都具有S形的函数曲线以及压缩输入值域的作用,所以又叫挤压型激活函数。

1 对数几率函数 Sigmoid Function

对率函数,在用于激活函数时常常被称为Sigmoid函数,因为它是最常用的Sigmoid函数。

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
优点

从函数图像来看,sigmoid函数的作用是将输入压缩到(0, 1)这个区间范围内,这种输出在0~1之间的函数可以用来模拟一些概率分布的情况。他还是一个连续函数,导数简单易求。

从数学上来看,Sigmoid函数对中央区的信号增益较大,对两侧区的信号增益小,在信号的特征空间映射上,有很好的效果。

从神经科学上来看,中央区酷似神经元的兴奋态,两侧区酷似神经元的抑制态,因而在神经网络学习方面,可以将重点特征推向中央区, 将非重点特征推向两侧区。

分类功能:我们经常听到这样的对白“你觉得这件事情成功概率有多大?”“我有六成把握能成功”。sigmoid函数在这里就起到了如何把一个数值转化成一个通俗意义上的把握的表示。值越大,那么这个神经元对于这张图里有这样一条线段的把握就越大,经过sigmoid函数之后的结果就越接近100%,也就是1这样一个值,表现在图里,也就是这个神经元越兴奋(亮)。

缺点

exp()计算代价大。

反向传播时梯度消失:从梯度图像中可以看到,sigmoid的梯度在两端都会接近于0,根据链式法则,如果传回的误差是δ,那么梯度传递函数是δ⋅a′(z),而a′(z)这时是零,也就是说整体的梯度是零。这也就很容易出现梯度消失的问题,并且这个问题可能导致网络收敛速度比较慢,比如采取MSE作为损失函数算法时。

给个纯粹数学的例子吧,假定我们的学习速率是0.2,sigmoid函数值是0.9,如果我们想把这个函数的值降到0.5,需要经过多少步呢?

我们先来做数值计算:

在这里插入图片描述
在这里插入图片描述
上半部分那条五彩斑斓的曲线就是迭代更新的过程了,一共迭代了多少次呢?根据程序统计,sigmoid迭代了67次才从0.9衰减到了接近0.5的水准。有同学可能会说了,才67次嘛,这个次数也不是很多啊!确实,从1层来看,这个速度还是可以接受的,但是神经网络只有这一层吗?多层叠加之后的sigmoid函数,因为反向传播的链式法则,两层的梯度相乘,每次更新的步长更小,需要的次数更多,也就是速度更加慢。如果还是没有反应过来的同学呢,可以先向下看relu函数的收敛速度。

此外,如果输入数据是(-1, 1)范围内的均匀分布的数据会导致什么样的结果呢?经过sigmoid函数处理之后这些数据的均值就从0变到了0.5,导致了均值的漂移,在很多应用中,这个性质是不好的。

2 Tanh函数

TanHyperbolic,双曲正切函数。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
优点

具有Sigmoid的所有优点。

无论从理论公式还是函数图像,这个函数都是一个和sigmoid非常相像的激活函数,他们的性质也确实如此。但是比起sigmoid,tanh减少了一个缺点,就是他本身是零均值的,也就是说,在传递过程中,输入数据的均值并不会发生改变,这就使他在很多应用中能表现出比sigmoid优异一些的效果。

缺点

exp()计算代价大。

梯度消失。

3 其它函数
在这里插入图片描述

半线性激活函数

又可以叫非饱和型激活函数。

1 ReLU函数

Rectified Linear Unit,修正线性单元,线性整流函数,斜坡函数。

在这里插入图片描述
在这里插入图片描述
仿生学原理

相关大脑方面的研究表明生物神经元的信息编码通常是比较分散及稀疏的。通常情况下,大脑中在同一时间大概只有1%-4%的神经元处于活跃状态。使用线性修正以及正则化(regularization)可以对机器神经网络中神经元的活跃度(即输出为正值)进行调试;相比之下,逻辑函数在输入为0时达到 ,即已经是半饱和的稳定状态,不够符合实际生物学对模拟神经网络的期望。不过需要指出的是,一般情况下,在一个使用修正线性单元(即线性整流)的神经网络中大概有50%的神经元处于激活态。

优点

1 反向导数恒等于1,更加有效率的反向传播梯度值,收敛速度快
2 避免梯度消失问题
3 计算简单,速度快
4 活跃度的分散性使得神经网络的整体计算成本下降

缺点

无界。

梯度很大的时候可能导致的神经元“死”掉。

而这个死掉的原因是什么呢?是因为很大的梯度导致更新之后的网络传递过来的输入是小于零的,从而导致relu的输出是0,计算所得的梯度是零,然后对应的神经元不更新,从而使relu输出恒为零,对应的神经元恒定不更新,等于这个relu失去了作为一个激活函数的梦想。问题的关键点就在于输入小于零时,relu回传的梯度是零,从而导致了后面的不更新。在学习率设置不恰当的情况下,很有可能网络中大部分神经元“死”掉,也就是说不起作用了。

用和sigmoid函数那里更新相似的算法步骤和参数,来模拟一下relu的梯度下降次数,也就是学习率α=0.2,希望函数值从0.9衰减到0.5,这样需要多少步呢?

在这里插入图片描述
也就是说,同样的学习速率,relu函数只需要两步就可以做到sigmoid需要67步才能衰减到的程度!

2 Leaky ReLU函数

PReLU,带泄露的线性整流函数。

在这里插入图片描述
在这里插入图片描述
优点

继承了ReLU函数的优点。

相比较于relu函数,leaky relu同样有收敛快速和运算复杂度低的优点,而且由于给了x<0时一个比较小的梯度α,使得x<0时依旧可以进行梯度传递和更新,可以在一定程度上避免神经元“死”掉的问题。

3 Softplus

在这里插入图片描述
在这里插入图片描述

ELU
在这里插入图片描述
在这里插入图片描述
BenIdentity
在这里插入图片描述
在这里插入图片描述

单入单出的双层神经网络

非线性回归

1 提出问题一

在工程实践中,我们最常遇到不是线性问题,而是非线性问题。例如下面这条正弦曲线:

在这里插入图片描述
在这里插入图片描述
问题:使用神经网络如何拟合一条有很强规律的曲线,比如正弦曲线?

2 提出问题二

前面的正弦函数,看上去是非常有规律的,也许单层神经网络很容易就做到了。如果是更复杂的曲线,单层神经网络还能轻易做到吗?比如下面这张图,给出如下一批训练数据,如何使用神经网络方法来拟合这条曲线?

在这里插入图片描述
在这里插入图片描述
原则上说,如果你有足够的耐心,愿意花很高的时间成本和计算资源,总可以用多项式回归的方式来解决这个问题,但是,在本章,我们将会学习另外一个定理:前馈神经网络的通用近似定理。

上面这条“蛇形”曲线,实际上是由下面这个公式添加噪音后生成的:

在这里插入图片描述
我们特意把数据限制在[0,1]之间,避免做归一化的麻烦。要是觉得这个公式还不够复杂,大家可以用更复杂的公式去自己做试验。

以上问题可以叫做非线性回归,即自变量X和因变量Y之间不是线性关系。常用的传统的处理方法有线性迭代法、分段回归法、迭代最小二乘法等。在神经网络中,解决这类问题的思路非常简单,就是使用带有一个隐层的两层神经网络。

3 回归模型的评估标准

基本数学概念
在这里插入图片描述
在这里插入图片描述
如果结果为正,表示X,Y是正相关。

回归模型评估标准

回归问题主要是求值,评价标准主要是看求得值与实际结果的偏差有多大,所以,回归问题主要以下方法来评价模型。

在这里插入图片描述
得出的值与样本数量有关系,假设有1000个测试样本,得到的值是120;如果只有100个测试样本,得到的值可能是11,我们不能说11就比120要好。

在这里插入图片描述
在这里插入图片描述

用多项式回归法拟合正弦曲线

1 多项式回归的概念

多项式回归有几种形式:

一元一次线性模型

因为只有一项,所以不能称为多项式了。它可以解决单变量的线性回归,我们在第4章学习过相关内容。其模型为:

在这里插入图片描述
多元一次多项式

多变量的线性回归,我们在第5章学习过相关内容。其模型为:
在这里插入图片描述
这里的多变量,是指样本数据的特征值为多个,上式中的x1,x2,…,xm代表了m个特征值。

一元多次多项式

单变量的非线性回归,比如上面这个正弦曲线的拟合问题,很明显不是线性问题,但是只有一个x特征值,所以不满足前两种形式。如何解决这种问题呢?

有一个定理:任意一个函数在一个较小的范围内,都可以用多项式任意逼近。因此在实际工程实践中,有时候可以不管y值与x值的数学关系究竟是什么,而是强行用回归分析方法进行近似的拟合。

那么如何得到更多的特征值呢?对于只有一个特征值的问题,人们发明了一种聪明的办法,就是把特征值的高次方作为另外的特征值,加入到回归分析中,用公式描述:

在这里插入图片描述
上式中x是原有的唯一特征值,xm是利用x的m次方作为额外的特征值,这样就把特征值的数量从1个变为m个。

在这里插入图片描述
可以看到公式4和上面的公式2是一样的,所以解决方案也一样。

多元多次多项式

多变量的非线性回归,其参数与特征组合繁复,但最终都可以归结为公式2和公式4的形式。

所以,不管是几元几次多项式,我们都可以使用第5章学到的方法来解决。在用代码具体实现之前,我们先学习一些前人总结的经验。

先看一个被经常拿出来讲解的例子:
在这里插入图片描述
一堆散点,看上去像是一条带有很大噪音的正弦曲线,从左上到右下,分别是1次多项式、2次多项式…10次多项式,其中:

第4、5、6、7图是比较理想的拟合
第1、2、3图欠拟合,多项式的次数不够高
第8、9、10图,多项式次数过高,过拟合了

再看下表中多项式的权重值:
在这里插入图片描述
项数越多,权重值越大。这是为什么呢?

在做多项式拟合之前,所有的特征值都会先做归一化,然后再获得x的平方值,三次方值等等。在归一化之后,x的值变成了[0,1]之间,那么x的平方值会比x值要小,x的三次方值会比x的平方值要小。假设x=0.5,x2=0.25,x3=0.125,所以次数越高,权重值会越大,特征值与权重值的乘积才会是一个不太小的数,以此来弥补特征值小的问题。

2 用二次多项式拟合

鉴于以上的认知,我们要考虑使用几次的多项式来拟合正弦曲线。在没有什么经验的情况下,可以先试一下二次多项式,即:

在这里插入图片描述
数据增强
在ch08.train.npz中,读出来的XTrain数组,只包含1列x的原始值,根据公式5,我们需要再增加一列x的平方值,所以代码如下:

import numpy as np
import matplotlib.pyplot as plt

from HelperClass.NeuralNet import *
from HelperClass.SimpleDataReader import *
from HelperClass.HyperParameters import *

file_name = "../../data/ch08.train.npz"

class DataReaderEx(SimpleDataReader):
    def Add(self):
        X = self.XTrain[:,]**2
        self.XTrain = np.hstack((self.XTrain, X))

从SimpleDataReader类中派生出子类DataReaderEx,然后添加Add()方法,先计算XTrain第一列的平方值放入矩阵X中,然后再把X合并到XTrain右侧,这样XTrain就变成了两列,第一列是x的原始值,第二列是x的平方值。

主程序

在主程序中,先加载数据,做数据增强,然后建立一个net,参数num_input=2,对应着XTrain中的两列数据,相当于两个特征值,

if __name__ == '__main__':
    dataReader = DataReaderEx(file_name)
    dataReader.ReadData()
    dataReader.Add()
    print(dataReader.XTrain.shape)

    # net
    num_input = 2
    num_output = 1
    params = HyperParameters(num_input, num_output, eta=0.2, max_epoch=10000, batch_size=10, eps=0.005, net_type=NetType.Fitting)
    net = NeuralNet(params)
    net.train(dataReader, checkpoint=10)
    ShowResult(net, dataReader, params.toString())

运行结果
在这里插入图片描述
拟合结果
在这里插入图片描述
从loss曲线上看,没有任何损失值下降的趋势;再看拟合情况,只拟合成了一条直线。这说明二次多项式不能满足要求。以下是最后几行的打印输出:

......
9979 49 0.09450642750766584
9989 49 0.09410913779071385
9999 49 0.09628814270449357
W= [[-1.72915813]
 [-0.16961507]]
B= [[0.98611283]]

3 用三次多项式拟合

三次多项式的公式:

在这里插入图片描述
在二次多项式的基础上,把训练数据的再增加一列x的三次方,作为一个新的特征。以下为数据增强代码:

class DataReaderEx(SimpleDataReader):
    def Add(self):
        X = self.XTrain[:,]**2
        self.XTrain = np.hstack((self.XTrain, X))
        X = self.XTrain[:,0:1]**3
        self.XTrain = np.hstack((self.XTrain, X))

同时不要忘记修改主过程参数中的num_input值:

num_input = 3

再次运行:

损失函数值
在这里插入图片描述
拟合结果
在这里插入图片描述
损失函数值下降得很平稳,说明网络训练效果还不错。拟合的结果也很令人满意,虽然红色线没有严丝合缝地落在蓝色样本点内,但是这完全是因为训练的次数不够多,有兴趣的读者可以修改超参后做进一步的试验。

以下为打印输出:

......
2349 49 0.005047530761174165
2359 49 0.005059504052006337
2369 49 0.0050611643902918856
2379 49 0.004949680631526745
W= [[ 10.49907256]
 [-31.06694195]
 [ 20.73039288]]
B= [[-0.07999603]]

可以观察到达到0.005的损失值,这个神经网络迭代了2379个epoch。而在二次多项式的试验中,用了10000次的迭代也没有达到要求。

4 用四次多项式拟合

在三次多项式得到比较满意的结果后,我们自然会想知道用四次多项式还会给我们带来惊喜吗?让我们一起试一试。

第一步依然是增加x的4次方作为特征值:

X = self.XTrain[:,0:1]**4
self.XTrain = np.hstack((self.XTrain, X))

第二步设置超参num_input=4,然后训练:

损失函数值
在这里插入图片描述
拟合结果
在这里插入图片描述

8259 49 0.005119146904922611
8269 49 0.005200637614155394
8279 49 0.00500000873141068
8289 49 0.0049964143635271635
W= [[  8.78717   ]
 [-20.55757649]
 [  1.28964911]
 [ 10.88610303]]
B= [[-0.04688634]]

从以上结果可以得到以下结论:

1 损失值在下降了一定程度都,一直处于平缓期,不再下降,说明网络能力到了一定的限制;

2 损失值达到0.005时,迭代了8289个epoch,比三次多项式的2379个epoch要多很多,说明四次多项式多出的一个特征值,没有给我们带来什么好处,反而是增加了网络训练的复杂度。

由此可以知道,多项式次数并不是越高越好,对不同的问题,有特定的限制,需要在实践中摸索,并无理论指导。

import numpy as np
import matplotlib.pyplot as plt

from HelperClass.NeuralNet_1_2 import *

file_name = "ch08.train.npz"

class DataReaderEx(DataReader_1_3):
    def Add(self):
        X = self.XTrain[:,]**2
        self.XTrain = np.hstack((self.XTrain, X))
        X = self.XTrain[:,0:1]**3
        self.XTrain = np.hstack((self.XTrain, X))
        X = self.XTrain[:,0:1]**4
        self.XTrain = np.hstack((self.XTrain, X))


def ShowResult(net, dataReader, title):
    # draw train data
    X,Y = dataReader.XTrain, dataReader.YTrain
    plt.plot(X[:,0], Y[:,0], '.', c='b')
    # create and draw visualized validation data
    TX1 = np.linspace(0,1,100).reshape(100,1)
    TX = np.hstack((TX1, TX1[:,]**2))
    TX = np.hstack((TX, TX1[:,]**3))
    TX = np.hstack((TX, TX1[:,]**4))

    TY = net.inference(TX)
    plt.plot(TX1, TY, 'x', c='r')
    plt.title(title)
    plt.show()
# end def

if __name__ == '__main__':
    dataReader = DataReaderEx(file_name)
    dataReader.ReadData()
    dataReader.Add()
    print(dataReader.XTrain.shape)

    # net
    num_input = 4
    num_output = 1
    params = HyperParameters_1_1(num_input, num_output, eta=0.2, max_epoch=10000, batch_size=10, eps=0.005, net_type=NetType.Fitting)
    net = NeuralNet_1_2(params)
    net.train(dataReader, checkpoint=10)
    ShowResult(net, dataReader, params.toString())

用多项式回归法拟合复合函数曲线

在本节中,我们尝试着用多项式解决问题二,拟合复杂的函数曲线。

在这里插入图片描述
再把这条“眼镜蛇形”曲线拿出来观察一下,不但有正弦式的波浪,还有线性的爬升,转折处也不是很平滑,所以难度很大。从正弦曲线的拟合经验来看,三次多项式以下肯定无法解决,所以我们可以从四次多项式开始试验。

1 用四次多项式拟合

代码与正弦函数拟合区别不大,不再赘述,我们本次主要说明解决问题的思路。

超参的设置情况:

  num_input = 4
    num_output = 1    
    params = HyperParameters(num_input, num_output, eta=0.2, max_epoch=10000, batch_size=10, eps=1e-3, net_type=NetType.Fitting)

最开始设置max_epoch=10,000,运行结果如下:

损失函数历史
在这里插入图片描述
曲线拟合结果
在这里插入图片描述
可以看到损失函数值还有下降的空间,并且拟合情况很糟糕。

9699 99 0.005015372990812302
9799 99 0.005149273288922641
9899 99 0.004994434937236122
9999 99 0.0049819495247358375
W= [[-0.70780292]
 [ 5.01194857]
 [-9.6191971 ]
 [ 6.07517269]]
B= [[-0.27837814]]

所以我们增加max_epoch到100,000再试一次:
在这里插入图片描述
从上图看,损失函数值到了一定程度后就不再下降了,说明网络能力有限。再看下面打印输出的具体数值,似乎0.005左右是一个极限。

99699 99 0.004770323351034788
99799 99 0.004701067202962632
99899 99 0.004685711600240152
99999 99 0.005299305272730845
W= [[ -2.18904889]
 [ 11.42075916]
 [-19.41933987]
 [ 10.88980241]]
B= [[-0.21280055]]

2 用六次多项式拟合

接下来跳过5次多项式,直接用6次多项式来拟合。这次不需要把max_epoch设置得很大,可以先试试50000个epoch:

在这里插入图片描述
打印输出:

999 99 0.005154576065966749
1999 99 0.004889156300531125
2999 99 0.004973132271850851
3999 99 0.004861537940283245
4999 99 0.00479384579608533
5999 99 0.004778358780580773
......
46999 99 0.004823741646005225
47999 99 0.004659351033377451
48999 99 0.0047460241904710935
49999 99 0.004669517756696059
W= [[-1.46506264]
 [ 6.60491296]
 [-6.53643709]
 [-4.29857685]
 [ 7.32734744]
 [-0.85129652]]
B= [[-0.21745171]]

从损失函数历史图看,好像损失值下降得比较理想,但是实际看打印输出时,损失值最开始几轮就已经是0.0047了,到了最后一轮,是0.0046,并不理想,说明网络能力还是不够。因此在这个级别上,不用再花时间继续试验了,应该还需要提高多项式次数。

3 用八次多项式拟合

再跳过7次多项式,直接使用8次多项式。先把max_epoch设置为50000试验一下:

在这里插入图片描述
损失函数值下降的趋势非常可喜,似乎还没有遇到什么瓶颈;拟合的效果也已经初步显现出来了。下方的打印输出,损失函数值已经可以突破0.004的下限了。

......
48499 99 0.003761445941675411
48999 99 0.0037995913365186595
49499 99 0.004086918553033752
49999 99 0.0037740488283595657
W= [[ -2.44771419]
 [  9.47854206]
 [ -3.75300184]
 [-14.39723202]
 [ -1.10074631]
 [ 15.09613263]
 [ 13.37017924]
 [-15.64867322]]
B= [[-0.16513259]]

根据以上情况,可以认为8次多项式很有可能得到比较理想的解,所以我们需要增加max_epoch数值,让网络得到充分的训练。好,设置max_epoch=1000000试一下!没错,是一百万次!开始运行后,大家就可以去做些别的事情,一两个小时之后再回来看结果。

在这里插入图片描述
从结果来看,损失函数值还有下降的空间和可能性,已经到了0.0016的水平(从后面的章节中可以知道,0.001的水平可以得到比较好的拟合效果),拟合效果也已经初步呈现出来了,所有转折的地方都可以复现,只是精度不够,相信更多的训练次数可以达到更好的效果。

......
995999 99 0.0015901747781799206
996999 99 0.0015873294515363775
997999 99 0.001596472587606985
998999 99 0.0015935143877633367
999999 99 0.0016124984420510522
W= [[  2.75832935]
 [-30.05663986]
 [ 99.68833781]
 [-85.95142109]
 [-71.42918867]
 [ 63.88516377]
 [104.44561608]
 [-82.7452897 ]]
B= [[-0.31611388]]

分析打印出的W权重值,x的原始特征值的权重值比后面的权重值小了一到两个数量级,这与归一化后x的高次幂的数值很小有关系。

至此,我们可以得出结论,多项式回归确实可以解决复杂曲线拟合问题,但是代价有些高,我们训练了一百万次,才得到初步满意的结果。下一节我们将要学习更好的方法。

import numpy as np
import matplotlib.pyplot as plt

from HelperClass.NeuralNet_1_2 import *

file_name = "ch09.train.npz"

class DataReaderEx(DataReader_1_3):
    def Add(self):
        X = self.XTrain[:,0:1]**2
        self.XTrain = np.hstack((self.XTrain, X))
        X = self.XTrain[:,0:1]**3
        self.XTrain = np.hstack((self.XTrain, X))
        X = self.XTrain[:,0:1]**4
        self.XTrain = np.hstack((self.XTrain, X))
        X = self.XTrain[:,0:1]**5
        self.XTrain = np.hstack((self.XTrain, X))
        X = self.XTrain[:,0:1]**6
        self.XTrain = np.hstack((self.XTrain, X))
        X = self.XTrain[:,0:1]**7
        self.XTrain = np.hstack((self.XTrain, X))
        X = self.XTrain[:,0:1]**8
        self.XTrain = np.hstack((self.XTrain, X))

def ShowResult(net, dataReader, title):
    # draw train data
    X,Y = dataReader.XTrain, dataReader.YTrain
    plt.plot(X[:,0], Y[:,0], '.', c='b')
    # create and draw visualized validation data
    TX1 = np.linspace(0,1,100).reshape(100,1)
    TX2 = np.hstack((TX1, TX1[:,]**2))
    TX3 = np.hstack((TX2, TX1[:,]**3))
    TX4 = np.hstack((TX3, TX1[:,]**4))
    TX5 = np.hstack((TX4, TX1[:,]**5))
    TX6 = np.hstack((TX5, TX1[:,]**6))
    TX7 = np.hstack((TX6, TX1[:,]**7))
    TX8 = np.hstack((TX7, TX1[:,]**8))
    TY = net.inference(TX8)
    plt.plot(TX1, TY, 'x', c='r')
    plt.title(title)
    plt.show()
#end def

if __name__ == '__main__':
    dataReader = DataReaderEx(file_name)
    dataReader.ReadData()
    dataReader.Add()
    print(dataReader.XTrain.shape)

    # net
    num_input = 8
    num_output = 1
    hp = HyperParameters_1_1(num_input, num_output, eta=0.2, max_epoch=50000, batch_size=10, eps=1e-3, net_type=NetType.Fitting)
    #params = HyperParameters(eta=0.2, max_epoch=1000000, batch_size=10, eps=1e-3, net_type=NetType.Fitting)
    net = NeuralNet_1_2(hp)
    net.train(dataReader, checkpoint=500)
    ShowResult(net, dataReader, "Polynomial")

双层神经网络实现非线性回归

万能近似定理

这里有一篇论文,Kurt Hornik在1991年发表的,说明了含有一个隐层的神经网络能拟合任意复杂函数。

https://www.sciencedirect.com/science/article/pii/089360809190009T

简言之:两层前馈神经网络(即一个隐层加一个输出层)和至少一层具有任何一种挤压性质的激活函数,只要隐层的神经元的数量足够,它可以以任意的精度来近似任何从一个有限维空间到另一个有限维空间的Borel可测函数。当然这个函数需要是单调递增有界的。

注意,它要求的是挤压性质的激活函数,也就是类似Sigmoid的函数,如果用ReLU函数不能实现这个效果。

万能近似定理意味着无论我们试图学习什么函数,我们知道一个大的MLP一定能够表示这个函数。然而,我们不能保证训练算法能够学得这个函数。即使MLP能够表示该函数,学习也可能因两个不同的原因而失败。

1 用于训练的优化算法可能找不到用于期望函数的参数值;
2 训练算法可能由于过拟合而选择了错误的函数。

根据“没有免费的午餐”定理,说明了没有普遍优越的机器学习算法。前馈网络提供了表示函数的万能系统,在这种意义上,给定一个函数,存在一个前馈网络能够近似该函数。但不存在万能的过程既能够验证训练集上的特殊样本,又能够选择一个函数来扩展到训练集上没有的点。

总之,具有单层的前馈网络足以表示任何函数,但是网络层可能大得不可实现,并且可能无法正确地学习和泛化。在很多情况下,使用更深的模型能够减少表示期望函数所需的单元的数量,并且可以减少泛化误差。

2 定义神经网络结构

通过观察样本数据的范围,x是在[0,1],y是[-0.5,0.5],这样我们就不用做数据归一化了。这条线看起来像一条处于攻击状态的眼镜蛇!由于是拟合任务,所以标签值y是一系列的实际数值,并不是0/1这样的特殊标记。

根据万能近似定理的要求,我们定义一个两层的神经网络,输入层不算,一个隐藏层,含3个神经元,一个输出层。

为什么用3个神经元呢?因为输入层只有一个特征值,我们不需要在隐层放很多的神经元,先用3个神经元试验一下。如果不够的话再增加,神经元数量是由超参控制的。

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
3 前向计算

在这里插入图片描述
在这里插入图片描述
其中,z是样本预测值,y是样本的标签值,这里的z是第二层的输出Z。

4 反向传播

我们比较一下本章的神经网络和第5章的神经网络的区别:

在这里插入图片描述
本章使用了真正的“网络”,而第5章充其量只是一个神经元而已。再看本章的网络的右半部分,从隐层到输出层的结构,和第5章的神经元结构一摸一样,只是输入为3个特征,而第5章的输入为两个特征。比较正向计算公式的话,也可以得到相同的结论。这就意味着反向传播的公式应该也是一样的。

由于我们第一次接触双层神经网络,所以需要推导一下反向传播的各个过程。看一下计算图,然后用链式求导法则反推。

求损失函数对隐层的反向误差

下面的内容是双层神经网络独有的内容,也是深度神经网络的基础,请大家仔细阅读体会。我们先看看正向计算和反向计算图:

在这里插入图片描述
图中:

1 蓝色矩形表示数值或矩阵
2 蓝色圆形表示计算单元
3 蓝色的箭头表示正向计算过程
4 红色的箭头表示反向计算过程

如果想计算W1和B1的反向误差,必须先得到Z1的反向误差,再向上追溯,可以看到Z1->A1->Z2->Loss这条线,Z1->A1是一个激活函数的运算,比较特殊,所以我们先看Loss->Z->A1如何解决。

4 代码实现

主要讲解神经网络NeuralNet2类的代码,其它的类都是辅助类。

前向计算

class NeuralNet2(object):
    def forward(self, batch_x):
        # layer 1
        self.Z1 = np.dot(batch_x, self.wb1.W) + self.wb1.B
        self.A1 = Sigmoid().forward(self.Z1)
        # layer 2
        self.Z2 = np.dot(self.A1, self.wb2.W) + self.wb2.B
        if self.hp.net_type == NetType.BinaryClassifier:
            self.A2 = Logistic().forward(self.Z2)
        elif self.hp.net_type == NetType.MultipleClassifier:
            self.A2 = Softmax().forward(self.Z2)
        else:   # NetType.Fitting
            self.A2 = self.Z2
        #end if
        self.output = self.A2

在Layer2中考虑了多种网络类型,在此我们暂时只关心NetType.Fitting类型。

反向传播

class NeuralNet2(object):
    def backward(self, batch_x, batch_y, batch_a):
        # 批量下降,需要除以样本数量,否则会造成梯度爆炸
        m = batch_x.shape[0]
        # 第二层的梯度输入 公式5
        dZ2 = self.A2 - batch_y
        # 第二层的权重和偏移 公式6
        self.wb2.dW = np.dot(self.A1.T, dZ2)/m 
        # 公式7 对于多样本计算,需要在横轴上做sum,得到平均值
        self.wb2.dB = np.sum(dZ2, axis=0, keepdims=True)/m 
        # 第一层的梯度输入 公式8
        d1 = np.dot(dZ2, self.wb2.W.T) 
        # 第一层的dZ 公式10
        dZ1,_ = Sigmoid().backward(None, self.A1, d1)
        # 第一层的权重和偏移 公式11
        self.wb1.dW = np.dot(batch_x.T, dZ1)/m
        # 公式12 对于多样本计算,需要在横轴上做sum,得到平均值
        self.wb1.dB = np.sum(dZ1, axis=0, keepdims=True)/m 

反向传播部分的代码完全按照公式推导的结果实现。

保存和加载权重矩阵数据

在训练结束后,或者每个epoch结束后,都可以选择保存训练好的权重矩阵值,避免每次使用时重复训练浪费时间。

而在初始化完毕神经网络后,可以立刻加载历史权重矩阵数据(前提是本次的神经网络设置与保存时的一致),这样可以在历史数据的基础上继续训练,不会丢失以前的进度。

def SaveResult(self):
    self.wb1.SaveResultValue(self.subfolder, "wb1")
    self.wb2.SaveResultValue(self.subfolder, "wb2")

def LoadResult(self):
    self.wb1.LoadResultValue(self.subfolder, "wb1")
    self.wb2.LoadResultValue(self.subfolder, "wb2")

辅助类
Activators - 激活函数类,包括Sigmoid/Tanh/Relu等激活函数的实现,以及Losistic/Softmax分类函数的实现

DataReader - 数据操作类,读取、归一化、验证集生成、获得指定类型批量数据

HyperParameters2 - 超参类,各层的神经元数量、学习率、批大小、网络类型、初始化方法等

class HyperParameters2(object):
    def __init__(self, n_input, n_hidden, n_output, 
                 eta=0.1, max_epoch=10000, batch_size=5, eps = 0.1, 
                 net_type = NetType.Fitting,
                 init_method = InitialMethod.Xavier):

LossFunction - 损失函数类,包含三种损失函数的代码实现

NeuralNet2 - 神经网络类,初始化、正向、反向、更新、训练、验证、测试等一系列方法

TrainingTrace - 训练记录类,记录训练过程中的损失函数值、验证精度

WeightsBias - 权重矩阵类,初始化、加载数据、保存数据

曲线拟合

在上一节我们已经建立和了神经网络及其辅助功能,现在我们先来做一下正弦曲线的拟合,然后再试验复合函数的曲线拟合。

正弦曲线的拟合

结果显示函数

此函数用于可视化测试拟合程度。

import numpy as np
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import Axes3D

from HelperClass2.NeuralNet2 import *
from HelperClass2.DataReader import *
x_data_name = "../../Data/ch08.train.npz"
y_data_name = "../../Data/ch08.test.npz"

def ShowResult(net, dataReader, title):
    # draw train data
    X,Y = dataReader.XTrain, dataReader.YTrain
    plt.plot(X[:,0], Y[:,0], '.', c='b')
    # create and draw visualized validation data
    TX = np.linspace(0,1,100).reshape(100,1)
    TY = net.inference(TX)
    plt.plot(TX, TY, 'x', c='r')
    plt.title(title)
    plt.show()
#end def

隐层只有一个神经元的情况

令num_hidden=1,并指定模型名称为"sin_111",再跑一次试验。

下图为损失函数曲线和验证集精度曲线,损失值到0.04附近就很难下降了,而2个神经元的网络损失值可以达到0.004,少一个数量级。验证集精度到82%左右,而2个神经元的网络可以达到97%。

在这里插入图片描述
下图为拟合情况的可视化图:

在这里插入图片描述
可以看到只有中间线性部分拟合了,两端的曲线部分没有拟合。

epoch=4899, total_iteration=220499
loss_train=0.027553, accuracy_train=0.850036
loss_valid=0.038706, accuracy_valid=0.821312
epoch=4949, total_iteration=222749
loss_train=0.033151, accuracy_train=0.778555
loss_valid=0.038575, accuracy_valid=0.821916
epoch=4999, total_iteration=224999
loss_train=0.015787, accuracy_train=0.943360
loss_valid=0.038609, accuracy_valid=0.821760
testing...
0.8575700023301912

打印输出最后的测试集精度值为85.7%,不是很理想。所以隐层1个神经元是基本不能工作的,这只比单层神经网络的线性拟合强一些,距离目标还差很远。

隐层有两个神经元的情况

if __name__ == '__main__':
    dataReader = DataReader(x_data_name, y_data_name)
    dataReader.ReadData()
    dataReader.GenerateValidationSet()

    n_input, n_hidden, n_output = 1, 2, 1
    eta, batch_size, max_epoch = 0.05, 10, 5000
    eps = 0.001

    hp = HyperParameters2(n_input, n_hidden, n_output, eta, max_epoch, batch_size, eps, NetType.Fitting, InitialMethod.Xavier)
    net = NeuralNet2(hp, "sin_121")
    #net.LoadResult()
    net.train(dataReader, 50, True)
    net.ShowTrainingTrace()
    ShowResult(net, dataReader, hp.toString())

初始化神经网络类的参数有两个,第一个是超参组合,第二个是指定模型专有名称,以便把结果保存在名称对应的子目录中。保存训练结果的代码在训练结束后自动调用,但是如果想加载历史训练结果,需要在主过程中手动调用,比如上面代码中注释的那一行:net.LoadResult()。这样的话,如果下次再训练,就可以在以前的基础上继续训练,不必从头开始。

注意在主过程代码中,我们指定了num_hidden=2,意为隐层神经元数量为2。、

运行结果

下图为损失函数曲线和验证集精度曲线,都比较正常:

在这里插入图片描述
下图为拟合情况的可视化图:

在这里插入图片描述
再看下面的打印输出结果,最后测试集的精度为98.8%。如果需要精度更高的话,可以增加迭代次数。

......
epoch=4899, total_iteration=220499
loss_train=0.003373, accuracy_train=0.985832
loss_valid=0.004621, accuracy_valid=0.978667
epoch=4949, total_iteration=222749
loss_train=0.002411, accuracy_train=0.990413
loss_valid=0.004409, accuracy_valid=0.979647
epoch=4999, total_iteration=224999
loss_train=0.007681, accuracy_train=0.971567
loss_valid=0.004366, accuracy_valid=0.979845
testing...
0.9881468747638157

2 复合函数的拟合

基本过程与正弦曲线相似,区别是这个例子要复杂不少,所以首先需要耐心,增大max_epoch的数值,多迭代几次。其次需要精心调参,找到最佳参数组合。

隐层只有两个神经元的情况

在这里插入图片描述
拟合情况很不理想,和正弦曲线只用一个神经元的情况类似。

epoch=99849, total_iteration=8986499
loss_train=0.000260, accuracy_train=0.992186
loss_valid=0.002989, accuracy_valid=0.809115
epoch=99899, total_iteration=8990999
loss_train=0.002313, accuracy_train=0.817327
loss_valid=0.003336, accuracy_valid=0.786957
epoch=99949, total_iteration=8995499
loss_train=0.001527, accuracy_train=0.925209
loss_valid=0.002794, accuracy_valid=0.821551
epoch=99999, total_iteration=8999999
loss_train=0.000751, accuracy_train=0.968484
loss_valid=0.003200, accuracy_valid=0.795622
testing...
0.8641114405898856

观察打印输出的损失值,有波动,久久徘徊在0.003附近不能下降,说明网络能力不够。

以下就是笔者找到的最佳组合:

隐层3个神经元
学习率=0.5
批量=10

隐层有三个神经元的情况

if __name__ == '__main__':
    dataReader = DataReader(x_data_name, y_data_name)
    dataReader.ReadData()
    dataReader.GenerateValidationSet()

    n_input, n_hidden, n_output = 1, 3, 1
    eta, batch_size, max_epoch = 0.5, 10, 10000
    eps = 0.001

    hp = HyperParameters2(n_input, n_hidden, n_output, eta, max_epoch, batch_size, eps, NetType.Fitting, InitialMethod.Xavier)
    net = NeuralNet2(hp, "model_131")

    net.train(dataReader, 50, True)
    net.ShowTrainingTrace()
    ShowResult(net, dataReader, hp.toString())

运行结果

下图为损失函数曲线和验证集精度曲线,都比较正常:

在这里插入图片描述
下图为拟合情况的可视化图:

在这里插入图片描述
再看下面的打印输出结果,最后测试集的精度为97.6%。如果需要精度更高的话,可以增加迭代次数。

......
epoch=4149, total_iteration=373499
loss_train=0.000293, accuracy_train=0.991577
loss_valid=0.001034, accuracy_valid=0.933974
epoch=4199, total_iteration=377999
loss_train=0.001152, accuracy_train=0.963756
loss_valid=0.000863, accuracy_valid=0.944908
testing...
0.9765910104463337

3 广义拟合

至此我们用两个可视化的例子完成了曲线拟合,验证了万能近似定理。但是,神经网络不是设计专门用于曲线拟合的,这只是牛刀小试而已,我们用简单的例子讲解了神经网络的功能,但是此功能完全可以用于多变量的复杂非线性回归。

“曲线”在这里是一个广义的概念,它可以代表二维平面上的数学曲线,也可以代表工程实践中的任何拟合问题,比如房价预测问题,影响房价的自变量可以达到20个左右,显然已经超出了线性回归的范畴,此时我们可以用多层神经网络来做预测,其准确度要比线性回归高很多。在后面我们会讲解这样的例子。

简言之,只要是数值拟合问题,确定不能用线性回归的话,都可以用非线性回归来尝试解决。

import numpy as np
import matplotlib.pyplot as plt

from HelperClass2.NeuralNet_2_0 import *

train_data_name = "ch09.train.npz"
test_data_name = "ch09.test.npz"

def ShowResult(net, dataReader, title):
    # draw train data
    X,Y = dataReader.XTrain, dataReader.YTrain
    plt.plot(X[:,0], Y[:,0], '.', c='b')
    # create and draw visualized validation data
    TX = np.linspace(0,1,100).reshape(100,1)
    TY = net.inference(TX)
    plt.plot(TX, TY, 'x', c='r')
    plt.title(title)
    plt.show()
#end def

if __name__ == '__main__':
    dataReader = DataReader_2_0(train_data_name, test_data_name)
    dataReader.ReadData()
    dataReader.GenerateValidationSet()

    n_input, n_hidden, n_output = 1, 3, 1
    eta, batch_size, max_epoch = 0.5, 10, 10000
    eps = 0.001

    hp = HyperParameters_2_0(n_input, n_hidden, n_output, eta, max_epoch, batch_size, eps, NetType.Fitting, InitialMethod.Xavier)
    net = NeuralNet_2_0(hp, "complex_131")

    net.train(dataReader, 50, True)
    net.ShowTrainingHistory()
    ShowResult(net, dataReader, hp.toString())

非线性回归的工作原理

1 多项式为何能拟合曲线

先回忆一下本章最开始讲的多项式回归法,它成功地用于正弦曲线和复合函数曲线的拟合,其基本工作原理是把单一特征值的高次方做为额外的特征值加入,使得神经网络可以得到附加的信息用于训练。实践证明其方法有效,但是当问题比较复杂时,需要高达8次方的附加信息,且训练时间也很长。

当我们使用双层神经网络时,在隐层只放置了三个神经元,就轻松解决了复合函数拟合的问题,效率高出十几倍,复杂度却降低了几倍。那么含有隐层的神经网络究竟是如何完成这个任务的呢?

我们以正弦曲线拟合为例来说明这个问题,首先看一下多项式回归方法的示意图:

在这里插入图片描述
在这里插入图片描述
我们可以回忆一下第5章学习的多变量线性回归问题,公式1实际上是把一维的x的特征信息,增加到了三维,然后再使用多变量线性回归来解决问题的。本来一维的特征只能得到线性的结果,但是三维的特征就可以得到非线性的结果,这就是多项式拟合的原理。

我们用具体的数值计算方式来理解一下其工作过程。

import numpy as np
import matplotlib.pyplot as plt

if __name__ == '__main__':
    x = np.linspace(0,1,10)
    w = 1.1
    b = 0.2
    y = x * w + b
    p1, = plt.plot(x,y, marker='.')

    x2 = x*x
    w2 = -0.5
    y2 = x * w + x2 * w2 + b
    p2, = plt.plot(x, y2, marker='s')

    x3 = x*x*x
    w3 = 2.3
    y3 = x * w + x2 * w2 + x3 * w3 + b
    p3, = plt.plot(x, y3, marker='x')

    plt.grid()
    plt.xlabel("x")
    plt.ylabel("y")
    plt.title("linear and non-linear")
    plt.legend([p1,p2,p3], ["x","x*x","x*x*x"])
    plt.show()

上述代码完成了如下任务:

1 定义[0,1]之间的等距的10个点

2使用w=1.1, b=0.2, 计算一个线性回归数值y

3 使用w=1.1, w2=-0.5, b=0.2, 计算一个二项式回归数值y2

4 使用w=1.1, w2=-0.5, w3=2.3, b=0.2, 计算一个三项式回归数值y3

5 绘制出三条曲线

在这里插入图片描述
可以清楚地看到:

蓝色直线是线性回归数值序列
红色曲线是二项式回归数值序列
绿色曲线是三项式回归数值序列

也就是说,我们只使用了同一个x序列的原始值,却可以得到三种不同数值序列,这就是多项式拟合的原理。

当多项式次数很高,原始数值训练足够宽的时候,甚至可以拟合出非单调的曲线,有兴趣的读者可以自己做个试验。

2 神经网络的非线性拟合工作原理

我们以正弦曲线的例子来讲解神经网络非线性回归的工作过程和原理。

比较一下下图,左侧为单特征多项式拟合的示意图,右侧为双层神经网络的示意图:

在这里插入图片描述
左侧图中,通过人为的方式,给Z的输入增加了x2和x3项。

右图中,通过线性变换的方式,把x变成了两部分:z11/a11,z12/a12,然后再通过一次线性变换把两者组合成为Z,这种方式和多项式回归非常类似:

1 隐层把x拆成不同的特征,根据问题复杂度觉得神经元数量;

2 隐层做一次激活函数的非线性变换;

3 输出层使用多变量线性回归,把隐层的输出当作输入特征值,再做一次线性变换,得出拟合结果。

与多项式回归不同的是,不需要指定变换参数,而是从训练中学习到参数,这样的话权重值不会大得离谱。

第一步 把X拆成两个线性序列z1和z2

原始值x有21个点:
在这里插入图片描述
通过以下线性变换,被分成了两个线性序列:

在这里插入图片描述

其中:

在这里插入图片描述
三个线性序列如下图所示:

在这里插入图片描述
这个运算相当于把特征值分解成两个部分,不太容易理解。打个不太恰当的比喻,有一个浮点数12.34,你可以把它拆成12和0.34两个部分,然后去分别做一些运算。另外一个例子就是,一张彩色图片上的黄色,我们普通人看到的就是黄色,但是画家会想到是红色和绿色的组合。

第二步 计算z1的激活函数值a1
在这里插入图片描述
在这里插入图片描述
z1还是一条直线,但是经过激活函数后的a1已经不是一条直线了。上面这张图由于z1的跨度大,所以a1的曲线程度不容易看出来。

第三步 计算z2的激活函数值a2

在这里插入图片描述
在这里插入图片描述
z2还是一条直线,但是经过激活函数后的a2已经明显看出是一条曲线了。

第四步 计算Z值

在这里插入图片描述
在这里插入图片描述
也就是说,相同x值的红点a1和绿点a2,经过公式6计算后得到蓝点z,所有这样经过计算出的蓝点就拟合出一条正弦曲线。

3 比较多项式回归和双层神经网络解法在这里插入图片描述

import numpy as np
import matplotlib.pyplot as plt

from HelperClass2.NeuralNet_2_0 import *

train_data_name = "ch08.train.npz"
test_data_name = "ch08.test.npz"


def ShowResult2D(net, title):
    count = 21

    TX = np.linspace(0, 1, count).reshape(count, 1)
    TY = net.inference(TX)

    print("TX=", TX)
    print("Z1=", net.Z1)
    print("A1=", net.A1)
    print("Z=", net.Z2)

    fig = plt.figure(figsize=(6, 6))
    p1, = plt.plot(TX, np.zeros((count, 1)), '.', c='black')
    p2, = plt.plot(TX, net.Z1[:, 0], '.', c='r')
    p3, = plt.plot(TX, net.Z1[:, 1], '.', c='g')
    plt.legend([p1, p2, p3], ["x", "z1", "z2"])
    plt.grid()
    plt.show()

    fig = plt.figure(figsize=(6, 6))
    p1, = plt.plot(TX, np.zeros((count, 1)), '.', c='black')
    p2, = plt.plot(TX, net.Z1[:, 0], '.', c='r')
    p3, = plt.plot(TX, net.A1[:, 0], 'x', c='r')
    plt.legend([p1, p2, p3], ["x", "z1", "a1"])
    plt.grid()
    plt.show()

    fig = plt.figure(figsize=(6, 6))
    p1, = plt.plot(TX, np.zeros((count, 1)), '.', c='black')
    p2, = plt.plot(TX, net.Z1[:, 1], '.', c='g')
    p3, = plt.plot(TX, net.A1[:, 1], 'x', c='g')
    plt.legend([p1, p2, p3], ["x", "z2", "a2"])
    plt.show()

    fig = plt.figure(figsize=(6, 6))
    p1, = plt.plot(TX, net.A1[:, 0], '.', c='r')
    p2, = plt.plot(TX, net.A1[:, 1], '.', c='g')
    p3, = plt.plot(TX, net.Z2[:, 0], 'x', c='blue')
    plt.legend([p1, p2, p3], ["a1", "a2", "z"])
    plt.show()


if __name__ == '__main__':
    dataReader = DataReader_2_0(train_data_name, test_data_name)
    dataReader.ReadData()
    dataReader.GenerateValidationSet()

    n_input, n_hidden, n_output = 1, 2, 1
    eta, batch_size, max_epoch = 0.05, 10, 5000
    eps = 0.001

    hp = HyperParameters_2_0(n_input, n_hidden, n_output, eta, max_epoch, batch_size, eps, NetType.Fitting,
                             InitialMethod.Xavier)
    net = NeuralNet_2_0(hp, "sin_121")

    net.LoadResult()
    print(net.wb1.W)
    print(net.wb1.B)
    print(net.wb2.W)
    print(net.wb2.B)

    # net.train(dataReader, 50, True)
    # net.ShowTrainingHistory_2_0()
    # ShowResult(net, dataReader, hp.toString())
    ShowResult2D(net, hp.toString())

超参数优化的初步认识

超参数优化(Hyperparameter Optimization)主要存在两方面的困难:

1 超参数优化是一个组合优化问题,无法像一般参数那样通过梯度下降方法来优化,也没有一种通用有效的优化方法。

2 评估一组超参数配置(Configuration)的时间代价非常高,从而导致一些优化方法(比如演化算法)在超参数优化中难以应用。

对于超参数的设置,比较简单的方法有人工搜索、网格搜索和随机搜索。

1 可调的参数

我们使用如下参数做第一次的训练:

在这里插入图片描述
上述表格中的参数,最终可以调节的其实只有三个:

1 隐层神经元数
2 学习率
3 批样本量

另外还有一个权重矩阵初始化方法需要特别注意,我们在下一个段落中讲解。

另外两个要提一下的参数,第一个是最大epoch数,根据不同的模型和案例会有所不同,在本例中10000次足以承载所有的超参组合了;另外一个是损失门限值是一个后验数值,也就是说笔者通过试验,事先知道了当eps=0.001时,会训练出精度可接受的模型来,这个问题在实践中也只能摸着石头过河,因为在训练一个特定模型之前,谁也不能假设它能到达的精度值是多少,损失函数值的下限也是通过多次试验,通过历史记录的趋势来估算出来的。

如果读者不了解神经网络中的基本原理,那么所谓“调参”就是碰运气了。今天咱们可以试着改变几个参数,来看看训练结果,以此来增加对神经网络中各种参数的了解。

避免权重矩阵初始化的影响

权重矩阵中的参数,是神经网络要学习的参数,所以不能称作超参数。

权重矩阵初始化是神经网络训练非常重要的环节之一,不同的初始化方法,甚至是相同的方法但不同的随机值,都会给结果带来或多或少的影响。

在后面的几组比较中,都是用Xavier方法初始化的。在两次参数完全相同的试验中,即使两次都使用Xavier初始化,因为权重矩阵参数的差异,也会得到不同的结果。为了避免这个随机性,我们在代码WeightsBias.py中使用了一个小技巧,调用下面这个函数:

class WeightsBias(object):
    def InitializeWeights(self, folder, create_new):
        self.folder = folder
        if create_new:
            self.__CreateNew()
        else:
            self.__LoadExistingParameters()
        # end if

    def __CreateNew(self):
        self.W, self.B = WeightsBias.InitialParameters(self.num_input, self.num_output, self.init_method)
        self.__SaveInitialValue()
        
    def __LoadExistingParameters(self):
        file_name = str.format("{0}\\{1}.npz", self.folder, self.initial_value_filename)
        w_file = Path(file_name)
        if w_file.exists():
            self.__LoadInitialValue()
        else:
            self.__CreateNew()

第一次调用InitializeWeights()时,会得到一个随机初始化矩阵。以后再次调用时,如果设置create_new=False,只要隐层神经元数量不变并且初始化方法不变,就会用第一次的初始化结果,否则后面的各种参数调整的结果就没有可比性了。

2 手动调整参数

手动调整超参数,我们必须了解超参数、训练误差、泛化误差和计算资源(内存和运行时间)之间的关系。

手动调整超参数的主要目标是调整模型的有效容量以匹配任务的复杂性。有效容量受限于3个因素:

1 模型的表示容量

2 学习算法与代价代价函数的匹配程度

3 代价函数和训练过程正则化模型的程度
具有更多网络层、每层有更多隐藏单元的模型具有较高的表示能力,能够表示更复杂的函数。

学习率是最重要的超参数。如果你只有时间调整一个超参数,那就调整学习率。

在这里插入图片描述
3 网格搜索

当有3个或更少的超参数时,常见的超参数搜索方法是网格搜索(grid search)。对于每个超参数,选择一个较小的有限值集去试验。然后,这些超参数的笛卡儿乘积(所有的排列组合)得到若干组超参数,网格搜索使用每组超参数训练模型。挑选验证集误差最小的超参数作为最好的超参数组合。

用学习率和隐层神经元数量来举例,横向为学习率,取值[0.1,0.3,0.5,0.7];纵向为隐层神经元数量,取值[2,4,8,12],在每个组合上测试验证集的精度。我们假设其中最佳的组合精度达到0.97,学习率=0.5,神经元数=8,那么这个组合就是我们需要的模型超参,可以拿到测试集上去做最终测试了。

以下数据为假设值:

在这里插入图片描述
针对我们这个曲线拟合问题,规模较小,模型简单,所以可以用上表列出的数据做搜索。对于大规模模型问题,学习率的取值集合可以是{0.1, 0.01, 0.001, 0.0001, 0.00001},隐层单元数集合可以是{50, 100, 200, 500, 1000, 2000},亦即在对数尺度上搜索,确定范围后,可以做进一步的小颗粒步长的搜索。

网格搜索带来的一个明显问题是,计算代价会随着超参数数量呈指数级增长。如果有m个超参数,每个最多取n个值,那么训练和估计所需的试验数将是O(n的m次方)。我们可以并行地进行实验,并且并行要求十分宽松(进行不同搜索的机器之间几乎没有必要进行通信)。令人遗憾的是,由于网格搜索指数级增长计算代价,即使是并行,我们也无法提供令人满意的搜索规模。

下面我们做一下具体的试验。

学习率的调整

我们固定其它参数,即隐层神经元ne=4、batch_size=10不变,改变学习率,来试验网络训练情况。为了节省时间,不做无限轮次的训练,而是设置eps=0.001为最低精度要求,一旦到达,就停止训练。

在这里插入图片描述
下面是损失函数值的曲线对比:

在这里插入图片描述
需要说明的是,对于本例的拟合曲线这个特定问题,较大的学习率可以带来很快的收敛速度,但是有两点:

但并不是对所有问题都这样,有的问题可能需要0.001或者更小的学习率
学习率大时,开始时收敛快,但是到了后来有可能会错失最佳解

批大小的调整

我们固定其它参数,即隐层神经元ne=4、eta=0.5不变,调整批大小,来试验网络训练情况,设置eps=0.001为精度要求。

在这里插入图片描述
下面是损失函数值的曲线对比:

在这里插入图片描述
合适的批样本量会带来较快的收敛,前提是我们固定了学习率。如果想用较大的批数据,底层数据库计算的速度较快,但是需要同时调整学习率,才会相应地提高收敛速度。

这个结论的前提是我们用了0.5的学习率,如果用0.1的话,将会得到不同结论。

隐层神经元数量的调整

我们固定其它参数,即batch_size=10、eta=0.5不变,调整隐层神经元的数量,来试验网络训练情况,设置eps=0.001为精度要求。

在这里插入图片描述
下面是损失函数值的曲线对比:

在这里插入图片描述
对于这个特定问题,隐层神经元个数越多,收敛速度越快。但实际上,这个比较不准确,因为隐层神经元数量的变化,会导致权重矩阵的尺寸变化,因此对于上述4种试验,权重矩阵的初始值都不一样,不具有很强的可比性。我们只需要明白神经元数量多可以提高网络的学习能力这一点就可以了。

4 随机搜索

随机搜索(Bergstra and Bengio,2012),是一个替代网格搜索的方法,并且编程简单,使用更方便,能更快地收敛到超参数的良好取值。

随机搜索过程如下:

首先,我们为每个超参 数定义一个边缘分布,例如,Bernoulli分布或范畴分布(分别对应着二元超参数或离散超参数),或者对数尺度上的均匀分布(对应着正实 值超参数)。例如,其中,u(a,b)表示区间(a,b)上均匀采样的样本。类似地,log_number_of_hidden_units可以从 u(log(50),log(2000))上采样。

与网格搜索不同,我们不需要离散化超参数的值。这允许我们在一个更大的集合上进行搜索,而不产生额外的计算代价。实际上,当有几个超参数对性能度量没有显著影响时,随机搜索相比于网格搜索指数级地高效。

Bergstra and Bengio(2012)进行了详细的研究并发现相比于网格搜索,随机搜索能够更快地减小验证集误差(就每个模型运行的试验数而 言)。

与网格搜索一样,我们通常会重复运行不同 版本的随机搜索,以基于前一次运行的结果改进下一次搜索。

随机搜索能比网格搜索更快地找到良好超参数的原因是,没有浪费的实验,不像网格搜索有时会对一个超参数的两个不同值(给定其他超参 数值不变)给出相同结果。在网格搜索中,其他超参数将在这两次实验中拥有相同的值,而在随机搜索中,它们通常会具有不同的值。因此,如果这两个值的变化所对应的验证集误差没有明显区别的话,网格搜索没有必要重复两个等价的实验,而随机搜索仍然会对其他超参数进行两次独立的探索。

贝叶斯优化是另外一种比较成熟技术,有兴趣的读者请自行学习。

验证与测试

1 基本概念

训练集 train set

A set of examples used for learning, which is to fit the parameters (i.e., weights) of the classifier.

用于模型训练的数据样本。

验证集 development set or dev set or validation set

A set of examples used to tune the parameters (i.e., architecture, not weights) of a classifier, for example to choose the number of hidden units in a neural network.

是模型训练过程中单独留出的样本集,它可以用于调整模型的超参数和用于对模型的能力进行初步评估。

在神经网络中,我们用验证数据集:

1 寻找最优的网络深度

2 或者决定反向传播算法的停止点

3 或者在神经网络中选择隐藏层神经元的数量

4 在普通的机器学习中常用的交叉验证 (Cross Validation) 就是把训练数据集本身再细分成不同的验证数据集去训练模型。

测试集 test set

A set of examples used only to assess the performance (generalization) of a fully specified classifier.

用来评估模最终模型的泛化能力。但不能作为调参、选择特征等算法相关的选择的依据。

三者之间的关系如下图所示:

在这里插入图片描述
一个形象的比喻:

训练集:课本,学生根据课本里的内容来掌握知识。训练集直接参与了模型调参的过程,显然不能用来反映模型真实的能力。即不能直接拿课本上的问题来考试,防止死记硬背课本的学生拥有最好的成绩,即防止过拟合。

验证集:作业,通过作业可以知道不同学生学习情况、进步的速度快慢。验证集参与了人工调参(超参数)的过程,也不能用来最终评判一个模型(刷题库的学生不能算是学习好的学生)。

测试集:考试,考的题是平常都没有见过,考察学生举一反三的能力。所以要通过最终的考试(测试集)来考察一个学(模)生(型)真正的能力(期末考试)。

但是仅凭一次考试就对模型的好坏进行评判显然是不合理的,所以接下来就要介绍交叉验证法。

2 交叉验证

传统的机器学习

在传统的机器学习中,我们经常用交叉验证(Cross Validation)的方法,比如把数据分成10份,V1-V10,其中V1-V9用来训练,V10用来验证。然后用V2-V10做训练,V1做验证…如此我们可以做10次训练和验证,大大增加了模型的可靠性。

这样的话,验证集也可以做训练,训练集数据也可以做验证,当样本很少时,这个很有用。

神经网络/深度学习
那么深度学习中的用法是什么呢?

比如在神经网络中,训练时到底迭代多少次停止呢?或者我们设置学习率为多少何时呢?或者用几个中间层,以及每个中间层用几个神经元呢?如何正则化?这些都是超参数设置,都可以用验证集来解决。

在咱们前面的学习中,一般使用loss小于门限值做为迭代终止条件,因为我们预先知道了这个门限值可以满足训练精度。但对于实际应用中的问题,没有先验的门限值可以参考,如何设定终止条件?此时,我们可以用验证集来验证一下准确率,假设只有90%的的准确率,那可能确实是局部最优解。这样我们可以继续迭代,寻找全局最优解。

举个糖炒栗子:一个BP神经网络,我们无法确定隐层的神经元数目,因为没有理论支持。此时可以这样做:

在这里插入图片描述
1 随机将训练数据分成K等份(通常建议K=10),得到D1,D2,Dk

2 对于一个模型M,选择D9为验证集,其它为训练集,训练若干轮,用D9验证,得到误差E。再训练,再用D9测试,如此N次。对N次的误差做平均,得到泛化误差

3 换一个不同参数的模型的组合,比如神经元数量,或者网络层数,激活函数,用D8去得到泛化误差

4 …一共验证10组组合

5 最后选择具有最小泛化误差的模型结构,用所有的 D0…D9 再次训练,成为最终模型,不用再验证

6 用测试集测试

3 留出法 Hold out

使用交叉验证的方法虽然比较保险,但是非常耗时,尤其是在大数据量时,训练出一个模型都要很长时间,没有可能去训练出10个模型再去比较。

在深度学习中,有另外一种方法使用验证集,称为留出法。亦即从训练数据中保留出验证样本集,主要用于解决过拟合情况,这部分数据不用于训练。如果训练数据的准确度持续增长,但是验证数据的准确度保持不变或者反而下降,说明神经网络亦即过拟合了,此时需要停止训练,用测试集做最终测试。

所以,训练步骤的伪代码如下:

for each epoch
    shuffle
    for each iteraion
        获得当前小批量数据
        前向计算
        反向传播
        更新梯度
        if is checkpoint
            用当前小批量数据计算训练集的loss值和accuracy值并记录
            计算验证集的loss值和accuracy值并记录
            如果loss值不再下降,停止训练
            如果accuracy值满足要求,停止训练
        end if
    end for
end for

从本章开始,我们将使用新的DataReader类来管理训练/测试数据,与前面的SimpleDataReader类相比,这个类有以下几个不同之处:

要求既有训练集,也有测试集
提供GenerateValidationSet()方法,可以从训练集中产生验证集
以上两个条件保证了我们在以后的训练中,可以使用本节中所描述的留出法,来监控整个训练过程。

关于三者的比例关系,在传统的机器学习中,三者可以是6:2:2。在深度学习中,一般要求样本数据量很大,所以可以给训练集更多的数据,比如8:1:1。

如果有些数据集已经给了你训练集和测试集,那就不关心其比例问题了,只需要从训练集中留出10%左右的验证集就可以了。

4 代码实现

定义DataReader类如下:

class DataReader(object):
    def __init__(self, train_file, test_file):
        self.train_file_name = train_file
        self.test_file_name = test_file
        self.num_train = 0        # num of training examples
        self.num_test = 0         # num of test examples
        self.num_validation = 0   # num of validation examples
        self.num_feature = 0      # num of features
        self.num_category = 0     # num of categories
        self.XTrain = None        # training feature set
        self.YTrain = None        # training label set
        self.XTest = None         # test feature set
        self.YTest = None         # test label set
        self.XTrainRaw = None     # training feature set before normalization
        self.YTrainRaw = None     # training label set before normalization
        self.XTestRaw = None      # test feature set before normalization
        self.YTestRaw = None      # test label set before normalization
        self.XVld = None          # validation feature set
        self.YVld = None          # validation lable set

X - 样本特征值数据
Y - 样本标签值数据

得到训练集和测试集

一般的数据集都有训练集和测试集,如果没有,需要从一个单一数据集中,随机抽取出一小部分作为测试集,剩下的一大部分作为训练集。

读取数据

def ReadData(self):
    train_file = Path(self.train_file_name)
    if train_file.exists():
        data = np.load(self.train_file_name)
        self.XTrainRaw = data["data"]
        self.YTrainRaw = data["label"]
        assert(self.XTrainRaw.shape[0] == self.YTrainRaw.shape[0])
        self.num_train = self.XTrainRaw.shape[0]
        self.num_feature = self.XTrainRaw.shape[1]
        self.num_category = len(np.unique(self.YTrainRaw))
        # this is for if no normalize requirment
        self.XTrain = self.XTrainRaw
        self.YTrain = self.YTrainRaw
    else:
        raise Exception("Cannot find train file!!!")
    #end if

    test_file = Path(self.test_file_name)
    if test_file.exists():
        data = np.load(self.test_file_name)
        self.XTestRaw = data["data"]
        self.YTestRaw = data["label"]
        assert(self.XTestRaw.shape[0] == self.YTestRaw.shape[0])
        self.num_test = self.XTestRaw.shape[0]
        # this is for if no normalize requirment
        self.XTest = self.XTestRaw
        self.YTest = self.YTestRaw
    else:
        raise Exception("Cannot find test file!!!")
    #end if

在读入原始数据后,数据存放在XTrainRaw、YTrainRaw、XTestRaw、YTestRaw中。由于有些数据不需要做归一化处理,所以,在读入数据集后,令:XTrain=XTrainRaw、YTrain=YTrainRaw、XTest=XTestRaw、YTest=YTestRaw,如此一来,就可以直接使用XTrain、YTrain、XTest、YTest做训练和测试了,避免不做归一化时上述4个变量为空。

特征值归一化

def NormalizeX(self):
x_merge = np.vstack((self.XTrainRaw, self.XTestRaw))
x_merge_norm = self.__NormalizeX(x_merge)
train_count = self.XTrainRaw.shape[0]
self.XTrain = x_merge_norm[0:train_count,:]
self.XTest = x_merge_norm[train_count:,:]

如果需要归一化处理,则XTrainRaw -> XTrain、YTrainRaw -> YTrain、XTestRaw -> XTest、YTestRaw -> YTest。注意需要把Train、Test同时归一化,如上面代码中,先把XTrainRaw和XTestRaw合并,一起做归一化,然后再拆开,这样可以保证二者的值域相同。

比如,假设XTrainRaw中的特征值只包含1、2、3三种值,在对其归一化时,1、2、3会变成0、0.5、1;而XTestRaw中的特征值只包含2、3、4三种值,在对其归一化时,2、3、4会变成0、0.5、1。这就造成了0、0.5、1这三个值的含义在不同数据集中不一样。

把二者merge后,就包含了1、2、3、4四种值,再做归一化,会变成0、0.333、0.666、1,在训练和测试时,就会使用相同的归一化值。

标签值归一化

根据不同的网络类型,标签值的归一化方法也不一样。

def NormalizeY(self, nettype, base=0):
    if nettype == NetType.Fitting:
        y_merge = np.vstack((self.YTrainRaw, self.YTestRaw))
        y_merge_norm = self.__NormalizeY(y_merge)
        train_count = self.YTrainRaw.shape[0]
        self.YTrain = y_merge_norm[0:train_count,:]
        self.YTest = y_merge_norm[train_count:,:]                
    elif nettype == NetType.BinaryClassifier:
        self.YTrain = self.__ToZeroOne(self.YTrainRaw, base)
        self.YTest = self.__ToZeroOne(self.YTestRaw, base)
    elif nettype == NetType.MultipleClassifier:
        self.YTrain = self.__ToOneHot(self.YTrainRaw, base)
        self.YTest = self.__ToOneHot(self.YTestRaw, base)

如果是Fitting任务,即线性回归、非线性回归,对标签值使用普通的归一化方法,把所有的值映射到[0,1]之间
如果是BinaryClassifier,即二分类任务,把标签值变成0或者1。base参数是指原始数据中负类的标签值。比如,原始数据的两个类别标签值是1、2,则base=1,把1、2变成0、1
如果是MultipleClassifier,即多分类任务,把标签值变成One-Hot编码。

生成验证集

def GenerateValidationSet(self, k = 10):
    self.num_validation = (int)(self.num_train / k)
    self.num_train = self.num_train - self.num_validation
    # validation set
    self.XVld = self.XTrain[0:self.num_validation]
    self.YVld = self.YTrain[0:self.num_validation]
    # train set
    self.XTrain = self.XTrain[self.num_validation:]
    self.YTrain = self.YTrain[self.num_validation:]

验证集是从归一化好的训练集中抽取出来的。上述代码假设XTrain已经做过归一化,并且样本是无序的。如果样本是有序的,则需要先打乱。

获得批量样本

def GetBatchTrainSamples(self, batch_size, iteration):
        start = iteration * batch_size
        end = start + batch_size
        batch_X = self.XTrain[start:end,:]
        batch_Y = self.YTrain[start:end,:]
        return batch_X, batch_Y

训练时一般采样Mini-batch梯度下降法,所以要指定批大小batch_size和当前批次iteration,就可以从已经打乱过的样本中获得当前批次的数据,在一个epoch中根据iterationd的递增调用此函数。

样本打乱

def Shuffle(self):
    seed = np.random.randint(0,100)
    np.random.seed(seed)
    XP = np.random.permutation(self.XTrain)
    np.random.seed(seed)
    YP = np.random.permutation(self.YTrain)
    self.XTrain = XP
    self.YTrain = YP

样本打乱操作只涉及到训练集,在每个epoch开始时调用此方法。打乱时,要注意特征值X和标签值Y是分开存放的,所以要使用相同的seed来打乱,保证打乱顺序后的特征值和标签值还是一一对应的。

  • 16
    点赞
  • 77
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值