Xiaowu/20231016 (#812)
* del ch9 * del 10,11,12 * del 7 step * del step 8 * del step 9
This commit is contained in:
Родитель
eea1c6fd01
Коммит
e8f6f6c039
|
@ -3,175 +3,11 @@
|
|||
|
||||
## 8.1 挤压型激活函数
|
||||
|
||||
这一类函数的特点是,当输入值域的绝对值较大的时候,其输出在两端是饱和的,都具有S形的函数曲线以及压缩输入值域的作用,所以叫挤压型激活函数,又可以叫饱和型激活函数。
|
||||
|
||||
在英文中,通常用Sigmoid来表示,原意是S型的曲线,在数学中是指一类具有压缩作用的S型的函数,在神经网络中,有两个常用的Sigmoid函数,一个是Logistic函数,另一个是Tanh函数。下面我们分别来讲解它们。
|
||||
以下为本小节目录,详情请参阅《智能之门》正版图书,高等教育出版社。
|
||||
|
||||
### 8.1.1 Logistic函数
|
||||
|
||||
对数几率函数(Logistic Function,简称对率函数)。
|
||||
|
||||
很多文字材料中通常把激活函数和分类函数混淆在一起说,有一个原因是:在二分类任务中最后一层使用的对率函数与在神经网络层与层之间连接的Sigmoid激活函数,是同样的形式。所以它既是激活函数,又是分类函数,是个特例。
|
||||
|
||||
对这个函数的叫法比较混乱,在本书中我们约定一下,凡是用到“Logistic”词汇的,指的是二分类函数;而用到“Sigmoid”词汇的,指的是本激活函数。
|
||||
|
||||
#### 公式
|
||||
|
||||
$$Sigmoid(z) = \frac{1}{1 + e^{-z}} \rightarrow a \tag{1}$$
|
||||
|
||||
#### 导数
|
||||
|
||||
$$Sigmoid'(z) = a(1 - a) \tag{2}$$
|
||||
|
||||
注意,如果是矩阵运算的话,需要在公式2中使用$\odot$符号表示按元素的矩阵相乘:$a\odot (1-a)$,后面不再强调。
|
||||
|
||||
推导过程如下:
|
||||
|
||||
令:$u=1,v=1+e^{-z}$ 则:
|
||||
|
||||
$$
|
||||
\begin{aligned}
|
||||
Sigmoid'(z)&= (\frac{u}{v})'=\frac{u'v-v'u}{v^2} \\\\
|
||||
&=\frac{0-(1+e^{-z})'}{(1+e^{-z})^2}=\frac{e^{-z}}{(1+e^{-z})^2} \\\\
|
||||
&=\frac{1+e^{-z}-1}{(1+e^{-z})^2}=\frac{1}{1+e^{-z}}-(\frac{1}{1+e^{-z}})^2 \\\\
|
||||
&=a-a^2=a(1-a)
|
||||
\end{aligned}
|
||||
$$
|
||||
|
||||
#### 值域
|
||||
|
||||
- 输入值域:$(-\infty, \infty)$
|
||||
- 输出值域:$(0,1)$
|
||||
- 导数值域:$(0,0.25]$
|
||||
|
||||
#### 函数图像
|
||||
|
||||
<img src="./img/8/sigmoid.png" ch="500" />
|
||||
|
||||
图8-3 Sigmoid函数图像
|
||||
|
||||
#### 优点
|
||||
|
||||
从函数图像来看,Sigmoid函数的作用是将输入压缩到 $(0,1)$ 这个区间范围内,这种输出在0~1之间的函数可以用来模拟一些概率分布的情况。它还是一个连续函数,导数简单易求。
|
||||
|
||||
从数学上来看,Sigmoid函数对中央区的信号增益较大,对两侧区的信号增益小,在信号的特征空间映射上,有很好的效果。
|
||||
|
||||
从神经科学上来看,中央区酷似神经元的兴奋态,两侧区酷似神经元的抑制态,因而在神经网络学习方面,可以将重点特征推向中央区,
|
||||
将非重点特征推向两侧区。
|
||||
|
||||
分类功能:我们经常听到这样的对白:
|
||||
|
||||
- 甲:“你觉得这件事情成功概率有多大?”
|
||||
- 乙:“我有六成把握能成功。”
|
||||
|
||||
Sigmoid函数在这里就起到了如何把一个数值转化成一个通俗意义上的“把握”的表示。z坐标值越大,经过Sigmoid函数之后的结果就越接近1,把握就越大。
|
||||
|
||||
#### 缺点
|
||||
|
||||
指数计算代价大。
|
||||
|
||||
反向传播时梯度消失:从梯度图像中可以看到,Sigmoid的梯度在两端都会接近于0,根据链式法则,如果传回的误差是$\delta$,那么梯度传递函数是$\delta \cdot a'$,而$a'$这时接近零,也就是说整体的梯度也接近零。这就出现梯度消失的问题,并且这个问题可能导致网络收敛速度比较慢。
|
||||
|
||||
给个纯粹数学的例子,假定我们的学习速率是0.2,Sigmoid函数值是0.9(处于饱和区了),如果我们想把这个函数的值降到0.5,需要经过多少步呢?
|
||||
|
||||
我们先来做数值计算:
|
||||
|
||||
1. 求出当前输入的值
|
||||
|
||||
$$a=\frac{1}{1 + e^{-z}} = 0.9$$
|
||||
$$z = \ln{9}$$
|
||||
|
||||
2. 求出当前梯度
|
||||
|
||||
$$\delta = a \times (1 - a) = 0.9 \times 0.1= 0.09$$
|
||||
|
||||
3. 根据梯度更新当前输入值
|
||||
|
||||
$$z_{new} = z - \eta \times \delta = \ln{9} - 0.2 \times 0.09 = \ln(9) - 0.018$$
|
||||
|
||||
4. 判断当前函数值是否接近0.5
|
||||
|
||||
$$a=\frac{1}{1 + e^{-z_{new}}} = 0.898368$$
|
||||
|
||||
5. 重复步骤2-3,直到当前函数值接近0.5
|
||||
|
||||
如果用一个程序来计算的话,需要迭代67次,才可以从0.9趋近0.5。如果对67次这个数字没概念的话,读者可以参看8.2节中关于ReLU函数的相关介绍。
|
||||
|
||||
此外,如果输入数据是(-1, 1)范围内的均匀分布的数据会导致什么样的结果呢?经过Sigmoid函数处理之后这些数据的均值就从0变到了0.5,导致了均值的漂移,在很多应用中,这个性质是不好的。
|
||||
|
||||
### 8.1.2 Tanh函数
|
||||
|
||||
TanHyperbolic,即双曲正切函数。
|
||||
|
||||
#### 公式
|
||||
|
||||
$$
|
||||
Tanh(z) = \frac{e^{z} - e^{-z}}{e^{z} + e^{-z}} = (\frac{2}{1 + e^{-2z}}-1) \rightarrow a \tag{3}
|
||||
$$
|
||||
|
||||
即
|
||||
|
||||
$$
|
||||
Tanh(z) = 2 \cdot Sigmoid(2z) - 1 \tag{4}
|
||||
$$
|
||||
|
||||
#### 导数公式
|
||||
|
||||
$$
|
||||
Tanh'(z) = (1 + a)(1 - a)
|
||||
$$
|
||||
|
||||
利用基本导数公式23,令:$u={e^{z}-e^{-z}},v=e^{z}+e^{-z}$ 则有:
|
||||
|
||||
$$
|
||||
\begin{aligned}
|
||||
Tanh'(z)&=\frac{u'v-v'u}{v^2} \\\\
|
||||
&=\frac{(e^{z}-e^{-z})'(e^{z}+e^{-z})-(e^{z}+e^{-z})'(e^{z}-e^{-z})}{(e^{z}+e^{-z})^2} \\\\
|
||||
&=\frac{(e^{z}+e^{-z})(e^{z}+e^{-z})-(e^{z}-e^{-z})(e^{z}-e^ {-z})}{(e^{z}+e^{-z})^2} \\\\
|
||||
&=\frac{(e^{z}+e^{-z})^2-(e^{z}-e^{-z})^2}{(e^{z}+e^{-z})^2} \\\\
|
||||
&=1-(\frac{(e^{z}-e^{-z}}{e^{z}+e^{-z}})^2=1-a^2
|
||||
\end{aligned}
|
||||
$$
|
||||
|
||||
#### 值域
|
||||
|
||||
- 输入值域:$(-\infty,\infty)$
|
||||
- 输出值域:$(-1,1)$
|
||||
- 导数值域:$(0,1)$
|
||||
|
||||
|
||||
#### 函数图像
|
||||
|
||||
图8-4是双曲正切的函数图像。
|
||||
|
||||
<img src="./img/8/tanh.png" ch="500" />
|
||||
|
||||
图8-4 双曲正切函数图像
|
||||
|
||||
#### 优点
|
||||
|
||||
具有Sigmoid的所有优点。
|
||||
|
||||
无论从理论公式还是函数图像,这个函数都是一个和Sigmoid非常相像的激活函数,他们的性质也确实如此。但是比起Sigmoid,Tanh减少了一个缺点,就是他本身是零均值的,也就是说,在传递过程中,输入数据的均值并不会发生改变,这就使他在很多应用中能表现出比Sigmoid优异一些的效果。
|
||||
|
||||
#### 缺点
|
||||
|
||||
exp指数计算代价大。梯度消失问题仍然存在。
|
||||
|
||||
### 8.1.3 其它函数
|
||||
|
||||
图8-5展示了其它S型函数,除了$Tanh(x)$以外,其它的基本不怎么使用,目的是告诉大家这类函数有很多,但是常用的只有Sigmoid和Tanh两个。
|
||||
|
||||
<img src="./img/8/others.png" />
|
||||
|
||||
图8-5 其它S型函数
|
||||
|
||||
再强调一下本书中的约定:
|
||||
|
||||
1. Sigmoid,指的是对数几率函数用于激活函数时的称呼;
|
||||
2. Logistic,指的是对数几率函数用于二分类函数时的称呼;
|
||||
3. Tanh,指的是双曲正切函数用于激活函数时的称呼。
|
||||
|
||||
### 代码位置
|
||||
|
||||
ch08, Level1
|
||||
|
|
|
@ -3,149 +3,11 @@
|
|||
|
||||
## 8.2 半线性激活函数
|
||||
|
||||
又可以叫非饱和型激活函数。
|
||||
以下为本小节目录,详情请参阅《智能之门》正版图书,高等教育出版社。
|
||||
|
||||
### 8.2.1 ReLU函数
|
||||
|
||||
Rectified Linear Unit,修正线性单元,线性整流函数,斜坡函数。
|
||||
|
||||
#### 公式
|
||||
|
||||
$$ReLU(z) = max(0,z) = \begin{cases}
|
||||
z, & z \geq 0 \\\\
|
||||
0, & z < 0
|
||||
\end{cases}$$
|
||||
|
||||
#### 导数
|
||||
|
||||
$$ReLU'(z) = \begin{cases} 1 & z \geq 0 \\\\ 0 & z < 0 \end{cases}$$
|
||||
|
||||
#### 值域
|
||||
|
||||
- 输入值域:$(-\infty, \infty)$
|
||||
- 输出值域:$(0,\infty)$
|
||||
- 导数值域:$\\{0,1\\}$
|
||||
|
||||
<img src="./img/8/relu.png"/>
|
||||
|
||||
图8-6 线性整流函数ReLU
|
||||
|
||||
#### 仿生学原理
|
||||
|
||||
相关大脑方面的研究表明生物神经元的信息编码通常是比较分散及稀疏的。通常情况下,大脑中在同一时间大概只有1%~4%的神经元处于活跃状态。使用线性修正以及正则化可以对机器神经网络中神经元的活跃度(即输出为正值)进行调试;相比之下,Sigmoid函数在输入为0时输出为0.5,即已经是半饱和的稳定状态,不够符合实际生物学对模拟神经网络的期望。不过需要指出的是,一般情况下,在一个使用修正线性单元(即线性整流)的神经网络中大概有50%的神经元处于激活态。
|
||||
|
||||
#### 优点
|
||||
|
||||
- 反向导数恒等于1,更加有效率的反向传播梯度值,收敛速度快;
|
||||
- 避免梯度消失问题;
|
||||
- 计算简单,速度快;
|
||||
- 活跃度的分散性使得神经网络的整体计算成本下降。
|
||||
|
||||
#### 缺点
|
||||
|
||||
无界。
|
||||
|
||||
梯度很大的时候可能导致的神经元“死”掉。
|
||||
|
||||
这个死掉的原因是什么呢?是因为很大的梯度导致更新之后的网络传递过来的输入是小于零的,从而导致ReLU的输出是0,计算所得的梯度是零,然后对应的神经元不更新,从而使ReLU输出恒为零,对应的神经元恒定不更新,等于这个ReLU失去了作为一个激活函数的作用。问题的关键点就在于输入小于零时,ReLU回传的梯度是零,从而导致了后面的不更新。在学习率设置不恰当的情况下,很有可能网络中大部分神经元“死”掉,也就是说不起作用了。
|
||||
|
||||
用和Sigmoid函数那里更新相似的算法步骤和参数,来模拟一下ReLU的梯度下降次数,也就是学习率$\eta = 0.2$,希望函数值从0.9衰减到0.5,这样需要多少步呢?
|
||||
|
||||
由于ReLU的导数为1,所以:
|
||||
|
||||
$$
|
||||
0.9-1\times 0.2=0.7 \\\\
|
||||
0.7-1\times 0.2=0.5
|
||||
$$
|
||||
|
||||
也就是说,同样的学习速率,ReLU函数只需要两步就可以做到Sigmoid需要67步才能达到的数值!
|
||||
|
||||
### 8.2.2 Leaky ReLU函数
|
||||
|
||||
LReLU,带泄露的线性整流函数。
|
||||
|
||||
#### 公式
|
||||
|
||||
$$LReLU(z) = \begin{cases} z & z \geq 0 \\\\ \alpha \cdot z & z < 0 \end{cases}$$
|
||||
|
||||
#### 导数
|
||||
|
||||
$$LReLU'(z) = \begin{cases} 1 & z \geq 0 \\\\ \alpha & z < 0 \end{cases}$$
|
||||
|
||||
#### 值域
|
||||
|
||||
输入值域:$(-\infty, \infty)$
|
||||
|
||||
输出值域:$(-\infty,\infty)$
|
||||
|
||||
导数值域:$\\{\alpha,1\\}$
|
||||
|
||||
#### 函数图像
|
||||
|
||||
函数图像如图8-7所示。
|
||||
|
||||
<img src="./img/8/leakyRelu.png"/>
|
||||
|
||||
图8-7 LeakyReLU的函数图像
|
||||
|
||||
#### 优点
|
||||
|
||||
继承了ReLU函数的优点。
|
||||
|
||||
Leaky ReLU同样有收敛快速和运算复杂度低的优点,而且由于给了$z<0$时一个比较小的梯度$\alpha$,使得$z<0$时依旧可以进行梯度传递和更新,可以在一定程度上避免神经元“死”掉的问题。
|
||||
|
||||
### 8.2.3 Softplus函数
|
||||
|
||||
#### 公式
|
||||
|
||||
$$Softplus(z) = \ln (1 + e^z)$$
|
||||
|
||||
#### 导数
|
||||
|
||||
$$Softplus'(z) = \frac{e^z}{1 + e^z}$$
|
||||
|
||||
####
|
||||
|
||||
输入值域:$(-\infty, \infty)$
|
||||
|
||||
输出值域:$(0,\infty)$
|
||||
|
||||
导数值域:$(0,1)$
|
||||
|
||||
#### 函数图像
|
||||
|
||||
Softplus的函数图像如图8-8所示。
|
||||
|
||||
<img src="./img/8/softplus.png"/>
|
||||
|
||||
图8-8 Softplus的函数图像
|
||||
|
||||
### 8.2.4 ELU函数
|
||||
|
||||
#### 公式
|
||||
|
||||
$$ELU(z) = \begin{cases} z & z \geq 0 \\ \alpha (e^z-1) & z < 0 \end{cases}$$
|
||||
|
||||
#### 导数
|
||||
|
||||
$$ELU'(z) = \begin{cases} 1 & z \geq 0 \\ \alpha e^z & z < 0 \end{cases}$$
|
||||
|
||||
#### 值域
|
||||
|
||||
输入值域:$(-\infty, \infty)$
|
||||
|
||||
输出值域:$(-\alpha,\infty)$
|
||||
|
||||
导数值域:$(0,1]$
|
||||
|
||||
#### 函数图像
|
||||
|
||||
ELU的函数图像如图8-9所示。
|
||||
|
||||
<img src="./img/8/elu.png"/>
|
||||
|
||||
图8-9 ELU的函数图像
|
||||
|
||||
### 代码位置
|
||||
|
||||
ch08, Level2
|
||||
|
|
|
@ -3,237 +3,13 @@
|
|||
|
||||
## 9.1 用多项式回归法拟合正弦曲线
|
||||
|
||||
以下为本小节目录,详情请参阅《智能之门》正版图书,高等教育出版社。
|
||||
|
||||
### 9.1.1 多项式回归的概念
|
||||
|
||||
多项式回归有几种形式:
|
||||
|
||||
#### 一元一次线性模型
|
||||
|
||||
因为只有一项,所以不能称为多项式了。它可以解决单变量的线性回归,我们在第4章学习过相关内容。其模型为:
|
||||
|
||||
$$z = x w + b \tag{1}$$
|
||||
|
||||
#### 多元一次多项式
|
||||
|
||||
多变量的线性回归,我们在第5章学习过相关内容。其模型为:
|
||||
|
||||
$$z = x_1 w_1 + x_2 w_2 + ...+ x_m w_m + b \tag{2}$$
|
||||
|
||||
这里的多变量,是指样本数据的特征值为多个,上式中的 $x_1,x_2,...,x_m$ 代表了m个特征值。
|
||||
|
||||
#### 一元多次多项式
|
||||
|
||||
单变量的非线性回归,比如上面这个正弦曲线的拟合问题,很明显不是线性问题,但是只有一个 $x$ 特征值,所以不满足前两种形式。如何解决这种问题呢?
|
||||
|
||||
有一个定理:任意一个函数在一个较小的范围内,都可以用多项式任意逼近。因此在实际工程实践中,有时候可以不管 $y$ 值与 $x$ 值的数学关系究竟是什么,而是强行用回归分析方法进行近似的拟合。
|
||||
|
||||
那么如何得到更多的特征值呢?对于只有一个特征值的问题,人们发明了一种聪明的办法,就是把特征值的高次方作为另外的特征值,加入到回归分析中,用公式描述:
|
||||
|
||||
$$z = x w_1 + x^2 w_2 + ... + x^m w_m + b \tag{3}$$
|
||||
|
||||
上式中x是原有的唯一特征值,$x^m$ 是利用 $x$ 的 $m$ 次方作为额外的特征值,这样就把特征值的数量从 $1$ 个变为 $m$ 个。
|
||||
|
||||
换一种表达形式,令:$x_1 = x,x_2=x^2,\ldots,x_m=x^m$,则:
|
||||
|
||||
$$z = x_1 w_1 + x_2 w_2 + ... + x_m w_m + b \tag{4}$$
|
||||
|
||||
可以看到公式4和上面的公式2是一样的,所以解决方案也一样。
|
||||
|
||||
#### 多元多次多项式
|
||||
|
||||
多变量的非线性回归,其参数与特征组合繁复,但最终都可以归结为公式2和公式4的形式。
|
||||
|
||||
所以,不管是几元几次多项式,我们都可以使用第5章学到的方法来解决。在用代码具体实现之前,我们先学习一些前人总结的经验。先看一个被经常拿出来讲解的例子,如图9-3所示。
|
||||
|
||||
<img src="./img/9/polynomial_10_pic.png" />
|
||||
|
||||
图9-3 对有噪音的正弦曲线的拟合
|
||||
|
||||
一堆散点,看上去像是一条带有很大噪音的正弦曲线,从左上到右下,分别是1次多项式、2次多项式......10次多项式,其中:
|
||||
|
||||
- 第4、5、6、7图是比较理想的拟合
|
||||
- 第1、2、3图欠拟合,多项式的次数不够高
|
||||
- 第8、9、10图,多项式次数过高,过拟合了
|
||||
|
||||
再看表9-3中多项式的权重值,表示了拟合的结果,标题头的数字表示使用了几次多项式,比如第2列有两个值,表示该多项式的拟合结果是:
|
||||
|
||||
$$
|
||||
y = 0.826x_1 -1.84x_2
|
||||
$$
|
||||
|
||||
表9-3 多项式训练结果的权重值
|
||||
|
||||
|1|2|3|4|5|6|7|8|9|10|
|
||||
|--:|--:|--:|--:|--:|--:|--:|--:|--:|--:|
|
||||
|-0.096|0.826|0.823|0.033|0.193|0.413|0.388|0.363|0.376|0.363|
|
||||
||-1.84|-1.82|9.68|5.03|-7.21|-4.50|1.61|-6.46|18.39|
|
||||
|||-0.017|-29.80|-7.17|90.05|57.84|-43.49|131.77|-532.78|
|
||||
||||19.85|-16.09|-286.93|-149.63|458.26|-930.65|5669.0|
|
||||
|||||17.98|327.00|62.56|-1669.06|3731.38|-29316.1|
|
||||
||||||-123.61|111.33|2646.22|-8795.97|84982.2|
|
||||
|||||||-78.31|-1920.56|11551.86|-145853|
|
||||
||||||||526.35|-7752.23|147000|
|
||||
|||||||||2069.6|-80265.3|
|
||||
||||||||||18296.6|
|
||||
|
||||
另外,从表9-3中还可以看到,项数越多,权重值越大。这是为什么呢?
|
||||
|
||||
在做多项式拟合之前,所有的特征值都会先做归一化,然后再获得x的平方值,三次方值等等。在归一化之后,x的值变成了[0,1]之间,那么x的平方值会比x值要小,x的三次方值会比x的平方值要小。假设$x=0.5,x^2=0.25,x^3=0.125$,所以次数越高,权重值会越大,特征值与权重值的乘积才会是一个不太小的数,以此来弥补特征值小的问题。
|
||||
|
||||
### 9.1.2 用二次多项式拟合
|
||||
|
||||
鉴于以上的认知,我们要考虑使用几次的多项式来拟合正弦曲线。在没有什么经验的情况下,可以先试一下二次多项式,即:
|
||||
|
||||
$$z = x w_1 + x^2 w_2 + b \tag{5}$$
|
||||
|
||||
#### 数据增强
|
||||
|
||||
在`ch08.train.npz`中,读出来的`XTrain`数组,只包含1列x的原始值,根据公式5,我们需要再增加一列x的平方值,所以代码如下:
|
||||
|
||||
```Python
|
||||
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`中的两列数据,相当于两个特征值,
|
||||
|
||||
```Python
|
||||
if __name__ == '__main__':
|
||||
dataReader = DataReaderEx(file_name)
|
||||
dataReader.ReadData()
|
||||
dataReader.Add()
|
||||
# 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())
|
||||
```
|
||||
|
||||
#### 运行结果
|
||||
|
||||
表9-4 二次多项式训练过程与结果
|
||||
|
||||
|损失函数值|拟合结果|
|
||||
|---|---|
|
||||
|<img src="./img/9/sin_loss_2p.png">|<img src="./img/9/sin_result_2p.png">|
|
||||
|
||||
从表9-4的损失函数曲线上看,没有任何损失值下降的趋势;再看拟合情况,只拟合成了一条直线。这说明二次多项式不能满足要求。以下是最后几行的打印输出:
|
||||
|
||||
```
|
||||
......
|
||||
9989 49 0.09410913779071385
|
||||
9999 49 0.09628814270449357
|
||||
W= [[-1.72915813]
|
||||
[-0.16961507]]
|
||||
B= [[0.98611283]]
|
||||
```
|
||||
|
||||
对此结论持有怀疑的读者,可以尝试着修改主程序中的各种超参数,比如降低学习率、增加循环次数等,来验证一下这个结论。
|
||||
|
||||
### 9.1.3 用三次多项式拟合
|
||||
|
||||
三次多项式的公式:
|
||||
|
||||
$$z = x w_1 + x^2 w_2 + x^3 w_3 + b \tag{6}$$
|
||||
|
||||
在二次多项式的基础上,把训练数据的再增加一列x的三次方,作为一个新的特征。以下为数据增强代码:
|
||||
|
||||
```Python
|
||||
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`值:
|
||||
|
||||
```Python
|
||||
num_input = 3
|
||||
```
|
||||
|
||||
再次运行,得到表9-5所示的结果。
|
||||
|
||||
表9-5 三次多项式训练过程与结果
|
||||
|
||||
|损失函数值|拟合结果|
|
||||
|---|---|
|
||||
|<img src="./img/9/sin_loss_3p.png">|<img src="./img/9/sin_result_3p.png">|
|
||||
|
||||
表9-5中左侧图显示损失函数值下降得很平稳,说明网络训练效果还不错。拟合的结果也很令人满意,虽然红色线没有严丝合缝地落在蓝色样本点内,但是这完全是因为训练的次数不够多,有兴趣的读者可以修改超参后做进一步的试验。
|
||||
|
||||
以下为打印输出:
|
||||
|
||||
```
|
||||
......
|
||||
2369 49 0.0050611643902918856
|
||||
2379 49 0.004949680631526745
|
||||
W= [[ 10.49907256]
|
||||
[-31.06694195]
|
||||
[ 20.73039288]]
|
||||
B= [[-0.07999603]]
|
||||
```
|
||||
|
||||
可以观察到达到0.005的损失值,这个神经网络迭代了2379个`epoch`。而在二次多项式的试验中,用了10000次的迭代也没有达到要求。
|
||||
|
||||
### 9.1.4 用四次多项式拟合
|
||||
|
||||
在三次多项式得到比较满意的结果后,我们自然会想知道用四次多项式还会给我们带来惊喜吗?让我们一起试一试。
|
||||
|
||||
第一步依然是增加x的4次方作为特征值:
|
||||
|
||||
```Python
|
||||
X = self.XTrain[:,0:1]**4
|
||||
self.XTrain = np.hstack((self.XTrain, X))
|
||||
```
|
||||
|
||||
第二步设置超参num_input=4,然后训练,得到表9-6的结果。
|
||||
|
||||
表9-6 四次多项式训练过程与结果
|
||||
|
||||
|损失函数值|拟合结果|
|
||||
|---|---|
|
||||
|<img src="./img/9/sin_loss_4p.png">|<img src="./img/9/sin_result_4p.png">|
|
||||
|
||||
|
||||
```
|
||||
......
|
||||
8279 49 0.00500000873141068
|
||||
8289 49 0.0049964143635271635
|
||||
W= [[ 8.78717 ]
|
||||
[-20.55757649]
|
||||
[ 1.28964911]
|
||||
[ 10.88610303]]
|
||||
B= [[-0.04688634]]
|
||||
```
|
||||
|
||||
### 9.1.5 结果比较
|
||||
|
||||
表9-7 不同项数的多项式拟合结果比较
|
||||
|
||||
|多项式次数|迭代数|损失函数值|
|
||||
|:---:|---:|---:|
|
||||
|2|10000|0.095|
|
||||
|3|2380|0.005|
|
||||
|4|8290|0.005|
|
||||
|
||||
从表9-7的结果比较中可以得到以下结论:
|
||||
|
||||
1. 二次多项式的损失值在下降了一定程度后,一直处于平缓期,不再下降,说明网络能力到了一定的限制,直到10000次迭代也没有达到目的;
|
||||
2. 损失值达到0.005时,四项式迭代了8290次,比三次多项式的2380次要多很多,说明四次多项式多出的一个特征值,没有给我们带来什么好处,反而是增加了网络训练的复杂度。
|
||||
|
||||
由此可以知道,多项式次数并不是越高越好,对不同的问题,有特定的限制,需要在实践中摸索,并无理论指导。
|
||||
|
||||
### 代码位置
|
||||
|
||||
ch09, Level1
|
||||
|
|
|
@ -3,154 +3,11 @@
|
|||
|
||||
## 9.2 用多项式回归法拟合复合函数曲线
|
||||
|
||||
还记得我们在本章最开始提出的两个问题吗?在上一节中我们解决了问题一,学习了用多项式拟合正弦曲线,在本节中,我们尝试着用多项式解决问题二,拟合复杂的函数曲线。
|
||||
|
||||
<img src="./img/9/Sample.png" ch="500" />
|
||||
|
||||
图9-4 样本数据可视化
|
||||
|
||||
再把图9-4所示的这条“眼镜蛇形”曲线拿出来观察一下,不但有正弦式的波浪,还有线性的爬升,转折处也不是很平滑,所以难度很大。从正弦曲线的拟合经验来看,三次多项式以下肯定无法解决,所以我们可以从四次多项式开始试验。
|
||||
以下为本小节目录,详情请参阅《智能之门》正版图书,高等教育出版社。
|
||||
|
||||
### 9.2.1 用四次多项式拟合
|
||||
|
||||
代码与正弦函数拟合方法区别不大,不再赘述,我们本次主要说明解决问题的思路。
|
||||
|
||||
超参的设置情况:
|
||||
|
||||
```Python
|
||||
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=10000`,运行结果如表9-8所示。
|
||||
|
||||
表9-8 四次多项式1万次迭代的训练结果
|
||||
|
||||
|损失函数历史|曲线拟合结果|
|
||||
|---|---|
|
||||
|<img src="./img/9/complex_loss_4_10k.png">|<img src="./img/9/complex_result_4_10k.png">|
|
||||
|
||||
可以看到损失函数值还有下降的空间,拟合情况很糟糕。以下是打印输出结果:
|
||||
|
||||
```
|
||||
......
|
||||
9899 99 0.004994434937236122
|
||||
9999 99 0.0049819495247358375
|
||||
W= [[-0.70780292]
|
||||
[ 5.01194857]
|
||||
[-9.6191971 ]
|
||||
[ 6.07517269]]
|
||||
B= [[-0.27837814]]
|
||||
```
|
||||
|
||||
所以我们增加`max_epoch`到100000再试一次。
|
||||
|
||||
表9-9 四次多项式10万次迭代的训练结果
|
||||
|
||||
|损失函数历史|曲线拟合结果|
|
||||
|---|---|
|
||||
|<img src="./img/9/complex_loss_4_100k.png">|<img src="./img/9/complex_result_4_100k.png">|
|
||||
|
||||
从表9-9中的左图看,损失函数值到了一定程度后就不再下降了,说明网络能力有限。再看下面打印输出的具体数值,在0.005左右是一个极限。
|
||||
|
||||
```
|
||||
......
|
||||
99899 99 0.004685711600240152
|
||||
99999 99 0.005299305272730845
|
||||
W= [[ -2.18904889]
|
||||
[ 11.42075916]
|
||||
[-19.41933987]
|
||||
[ 10.88980241]]
|
||||
B= [[-0.21280055]]
|
||||
```
|
||||
|
||||
### 9.2.2 用六次多项式拟合
|
||||
|
||||
接下来跳过5次多项式,直接用6次多项式来拟合。这次不需要把`max_epoch`设置得很大,可以先试试50000个`epoch`。
|
||||
|
||||
表9-10 六次多项式5万次迭代的训练结果
|
||||
|
||||
|损失函数历史|曲线拟合结果|
|
||||
|---|---|
|
||||
|<img src="./img/9/complex_loss_6_50k.png">|<img src="./img/9/complex_result_6_50k.png">|
|
||||
|
||||
打印输出:
|
||||
|
||||
```
|
||||
999 99 0.005154576065966749
|
||||
1999 99 0.004889156300531125
|
||||
......
|
||||
48999 99 0.0047460241904710935
|
||||
49999 99 0.004669517756696059
|
||||
W= [[-1.46506264]
|
||||
[ 6.60491296]
|
||||
[-6.53643709]
|
||||
[-4.29857685]
|
||||
[ 7.32734744]
|
||||
[-0.85129652]]
|
||||
B= [[-0.21745171]]
|
||||
```
|
||||
|
||||
从表9-10的损失函数历史图看,损失值下降得比较理想,但是实际看打印输出时,损失值最开始几轮就已经是0.0047了,到了最后一轮,是0.0046,并不理想,说明网络能力还是不够。因此在这个级别上,不用再花时间继续试验了,应该还需要提高多项式次数。
|
||||
|
||||
### 9.2.3 用八次多项式拟合
|
||||
|
||||
再跳过7次多项式,直接使用8次多项式。先把`max_epoch`设置为50000试验一下。
|
||||
|
||||
表9-11 八项式5万次迭代的训练结果
|
||||
|
||||
|损失函数历史|曲线拟合结果|
|
||||
|---|---|
|
||||
|<img src="./img/9/complex_loss_8_50k.png">|<img src="./img/9/complex_result_8_50k.png">|
|
||||
|
||||
表9-11中损失函数值下降的趋势非常可喜,似乎还没有遇到什么瓶颈,仍有下降的空间,并且拟合的效果也已经初步显现出来了。
|
||||
|
||||
再看下面的打印输出,损失函数值已经可以突破0.004的下限了。
|
||||
|
||||
```
|
||||
......
|
||||
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`试一下!没错,是一百万次!开始运行后,大家就可以去做些别的事情,一两个小时之后再回来看结果。
|
||||
|
||||
表9-12 八项式100万次迭代的训练结果
|
||||
|
||||
|损失函数历史|曲线拟合结果|
|
||||
|---|---|
|
||||
|<img src="./img/9/complex_loss_8_1M.png">|<img src="./img/9/complex_result_8_1M.png">|
|
||||
|
||||
从表9-12的结果来看,损失函数值还有下降的空间和可能性,已经到了0.0016的水平(从后面的章节中可以知道,0.001的水平可以得到比较好的拟合效果),拟合效果也已经初步呈现出来了,所有转折的地方都可以复现,只是精度不够,相信更多的训练次数可以达到更好的效果。
|
||||
|
||||
```
|
||||
......
|
||||
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的高次幂的数值很小有关系。
|
||||
|
||||
至此,我们可以得出结论,多项式回归确实可以解决复杂曲线拟合问题,但是代价有些高,我们训练了一百万次,才得到初步满意的结果。下一节我们将要学习更好的方法。
|
||||
|
||||
### 代码位置
|
||||
|
||||
ch09, Level3
|
||||
|
|
|
@ -3,232 +3,9 @@
|
|||
|
||||
## 9.3 验证与测试
|
||||
|
||||
以下为本小节目录,详情请参阅《智能之门》正版图书,高等教育出版社。
|
||||
|
||||
### 9.3.1 基本概念
|
||||
|
||||
#### 训练集
|
||||
|
||||
Training Set,用于模型训练的数据样本。
|
||||
|
||||
#### 验证集
|
||||
|
||||
Validation Set,或者叫做Dev Set,是模型训练过程中单独留出的样本集,它可以用于调整模型的超参数和用于对模型的能力进行初步评估。
|
||||
|
||||
在神经网络中,验证数据集用于:
|
||||
|
||||
- 寻找最优的网络深度
|
||||
- 或者决定反向传播算法的停止点
|
||||
- 或者在神经网络中选择隐藏层神经元的数量
|
||||
- 在普通的机器学习中常用的交叉验证(Cross Validation)就是把训练数据集本身再细分成不同的验证数据集去训练模型。
|
||||
|
||||
#### 测试集
|
||||
|
||||
Test Set,用来评估最终模型的泛化能力。但不能作为调参、选择特征等算法相关的选择的依据。
|
||||
|
||||
三者之间的关系如图9-5所示。
|
||||
|
||||
<img src="./img/9/dataset.png" />
|
||||
|
||||
图9-5 训练集、验证集、测试集的关系
|
||||
|
||||
一个形象的比喻:
|
||||
|
||||
- 训练集:课本,学生根据课本里的内容来掌握知识。训练集直接参与了模型调参的过程,显然不能用来反映模型真实的能力。即不能直接拿课本上的问题来考试,防止死记硬背课本的学生拥有最好的成绩,即防止过拟合。
|
||||
|
||||
- 验证集:作业,通过作业可以知道不同学生学习情况、进步的速度快慢。验证集参与了人工调参(超参数)的过程,也不能用来最终评判一个模型(刷题库的学生不能算是学习好的学生)。
|
||||
|
||||
- 测试集:考试,考的题是平常都没有见过,考察学生举一反三的能力。所以要通过最终的考试(测试集)来考察一个学型(模生)真正的能力(期末考试)。
|
||||
|
||||
考试题是学生们平时见不到的,也就是说在模型训练时看不到测试集。
|
||||
|
||||
### 9.3.2 交叉验证
|
||||
|
||||
#### 传统的机器学习
|
||||
|
||||
在传统的机器学习中,我们经常用交叉验证的方法,比如把数据分成10份,$V_1\sim V_{10}$,其中 $V_1 \sim V_9$ 用来训练,$V_{10}$ 用来验证。然后用 $V_2\sim V_{10}$ 做训练,$V_1$ 做验证……如此我们可以做10次训练和验证,大大增加了模型的可靠性。
|
||||
|
||||
这样的话,验证集也可以做训练,训练集数据也可以做验证,当样本很少时,这个方法很有用。
|
||||
|
||||
#### 神经网络/深度学习
|
||||
|
||||
那么深度学习中的用法是什么呢?
|
||||
|
||||
比如在神经网络中,训练时到底迭代多少次停止呢?或者我们设置学习率为多少合适呢?或者用几个中间层,以及每个中间层用几个神经元呢?如何正则化?这些都是超参数设置,都可以用验证集来解决。
|
||||
|
||||
在咱们前面的学习中,一般使用损失函数值小于门限值做为迭代终止条件,因为通过前期的训练,笔者预先知道了这个门限值可以满足训练精度。但对于实际应用中的问题,没有先验的门限值可以参考,如何设定终止条件?此时,我们可以用验证集来验证一下准确率,假设只有90%的准确率,可能是局部最优解。这样我们可以继续迭代,寻找全局最优解。
|
||||
|
||||
举个例子:一个BP神经网络,我们无法确定隐层的神经元数目,因为没有理论支持。此时可以按图9-6的示意图这样做。
|
||||
|
||||
<img src="./img/9/CrossValidation.png" ch="500" />
|
||||
|
||||
图9-6 交叉训练的数据配置方式
|
||||
|
||||
1. 随机将训练数据分成K等份(通常建议 $K=10$),得到$D_0,D_1,D_9$;
|
||||
2. 对于一个模型M,选择 $D_9$ 为验证集,其它为训练集,训练若干轮,用 $D_9$ 验证,得到误差 $E$。再训练,再用 $D_9$ 测试,如此N次。对N次的误差做平均,得到平均误差;
|
||||
3. 换一个不同参数的模型的组合,比如神经元数量,或者网络层数,激活函数,重复2,但是这次用 $D_8$ 去得到平均误差;
|
||||
4. 重复步骤2,一共验证10组组合;
|
||||
5. 最后选择具有最小平均误差的模型结构,用所有的 $D_0 \sim D_9$ 再次训练,成为最终模型,不用再验证;
|
||||
6. 用测试集测试。
|
||||
|
||||
### 9.3.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%左右的验证集就可以了。
|
||||
|
||||
### 9.3.4 代码实现
|
||||
|
||||
定义DataReader类如下:
|
||||
|
||||
```Python
|
||||
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
|
||||
```
|
||||
|
||||
命名规则:
|
||||
|
||||
1. 以`num_`开头的表示一个整数,后面跟着数据集的各种属性的名称,如训练集(`num_train`)、测试集(`num_test`)、验证集(`num_validation`)、特征值数量(`num_feature`)、分类数量(`num_category`);
|
||||
2. `X`表示样本特征值数据,`Y`表示样本标签值数据;
|
||||
3. `Raw`表示没有经过归一化的原始数据。
|
||||
|
||||
#### 得到训练集和测试集
|
||||
|
||||
一般的数据集都有训练集和测试集,如果没有,需要从一个单一数据集中,随机抽取出一小部分作为测试集,剩下的一大部分作为训练集,一旦测试集确定后,就不要再更改。然后在训练过程中,从训练集中再抽取一小部分作为验证集。
|
||||
|
||||
#### 读取数据
|
||||
|
||||
```Python
|
||||
def ReadData(self):
|
||||
train_file = Path(self.train_file_name)
|
||||
if train_file.exists():
|
||||
...
|
||||
|
||||
test_file = Path(self.test_file_name)
|
||||
if test_file.exists():
|
||||
...
|
||||
```
|
||||
|
||||
在读入原始数据后,数据存放在`XTrainRaw`、`YTrainRaw`、`XTestRaw`、`YTestRaw`中。由于有些数据不需要做归一化处理,所以,在读入数据集后,令:`XTrain=XTrainRaw`、`YTrain=YTrainRaw`、`XTest=XTestRaw`、`YTest=YTestRaw`,如此一来,就可以直接使用`XTrain`、`YTrain`、`XTest`、`YTest`做训练和测试了,避免不做归一化时上述4个变量为空。
|
||||
|
||||
#### 特征值归一化
|
||||
|
||||
```Python
|
||||
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,在训练和测试时,就会使用相同的归一化值。
|
||||
|
||||
#### 标签值归一化
|
||||
|
||||
根据不同的网络类型,标签值的归一化方法也不一样。
|
||||
|
||||
```Python
|
||||
def NormalizeY(self, nettype, base=0):
|
||||
if nettype == NetType.Fitting:
|
||||
...
|
||||
elif nettype == NetType.BinaryClassifier:
|
||||
...
|
||||
elif nettype == NetType.MultipleClassifier:
|
||||
...
|
||||
```
|
||||
|
||||
- 如果是`Fitting`任务,即线性回归、非线性回归,对标签值使用普通的归一化方法,把所有的值映射到[0,1]之间
|
||||
- 如果是`BinaryClassifier`,即二分类任务,把标签值变成0或者1。`base`参数是指原始数据中负类的标签值。比如,原始数据的两个类别标签值是1、2,则`base=1`,把1、2变成0、1
|
||||
- 如果是`MultipleClassifier`,即多分类任务,把标签值变成One-Hot编码。
|
||||
|
||||
#### 生成验证集
|
||||
|
||||
```Python
|
||||
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`已经做过归一化,并且样本是无序的。如果样本是有序的,则需要先打乱。
|
||||
|
||||
#### 获得批量样本
|
||||
```Python
|
||||
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中根据iteration的递增调用此函数。
|
||||
|
||||
#### 样本打乱
|
||||
```Python
|
||||
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`来打乱,保证打乱顺序后的特征值和标签值还是一一对应的。
|
||||
|
|
|
@ -3,397 +3,16 @@
|
|||
|
||||
## 9.4 双层神经网络实现非线性回归
|
||||
|
||||
以下为本小节目录,详情请参阅《智能之门》正版图书,高等教育出版社。
|
||||
|
||||
### 9.4.1 万能近似定理
|
||||
|
||||
万能近似定理(universal approximation theorem) $^{[1]}$,是深度学习最根本的理论依据。它证明了在给定网络具有足够多的隐藏单元的条件下,配备一个线性输出层和一个带有任何“挤压”性质的激活函数(如Sigmoid激活函数)的隐藏层的前馈神经网络,能够以任何想要的误差量近似任何从一个有限维度的空间映射到另一个有限维度空间的Borel可测的函数。
|
||||
|
||||
前馈网络的导数也可以以任意好地程度近似函数的导数。
|
||||
|
||||
万能近似定理其实说明了理论上神经网络可以近似任何函数。但实践上我们不能保证学习算法一定能学习到目标函数。即使网络可以表示这个函数,学习也可能因为两个不同的原因而失败:
|
||||
|
||||
1. 用于训练的优化算法可能找不到用于期望函数的参数值;
|
||||
2. 训练算法可能由于过拟合而选择了错误的函数。
|
||||
|
||||
根据“没有免费的午餐”定理,说明了没有普遍优越的机器学习算法。前馈网络提供了表示函数的万能系统,在这种意义上,给定一个函数,存在一个前馈网络能够近似该函数。但不存在万能的过程既能够验证训练集上的特殊样本,又能够选择一个函数来扩展到训练集上没有的点。
|
||||
|
||||
总之,具有单层的前馈网络足以表示任何函数,但是网络层可能大得不可实现,并且可能无法正确地学习和泛化。在很多情况下,使用更深的模型能够减少表示期望函数所需的单元的数量,并且可以减少泛化误差。
|
||||
|
||||
### 9.4.2 定义神经网络结构
|
||||
|
||||
本节的目的是要用神经网络完成图9-1和图9-2中的曲线拟合。
|
||||
|
||||
根据万能近似定理的要求,我们定义一个两层的神经网络,输入层不算,一个隐藏层,含3个神经元,一个输出层。图9-7显示了此次用到的神经网络结构。
|
||||
|
||||
<img src="./img/9/nn.png" />
|
||||
|
||||
图9-7 单入单出的双层神经网络
|
||||
|
||||
为什么用3个神经元呢?这也是笔者经过多次试验的最佳结果。因为输入层只有一个特征值,我们不需要在隐层放很多的神经元,先用3个神经元试验一下。如果不够的话再增加,神经元数量是由超参控制的。
|
||||
|
||||
#### 输入层
|
||||
|
||||
输入层就是一个标量x值,如果是成批输入,则是一个矢量或者矩阵,但是特征值数量总为1,因为只有一个横坐标值做为输入。
|
||||
|
||||
$$X = (x)$$
|
||||
|
||||
#### 权重矩阵W1/B1
|
||||
|
||||
$$
|
||||
W1=
|
||||
\begin{pmatrix}
|
||||
w1_{11} & w1_{12} & w1_{13}
|
||||
\end{pmatrix}
|
||||
$$
|
||||
|
||||
$$
|
||||
B1=
|
||||
\begin{pmatrix}
|
||||
b1_{1} & b1_{2} & b1_{3}
|
||||
\end{pmatrix}
|
||||
$$
|
||||
|
||||
#### 隐层
|
||||
|
||||
我们用3个神经元:
|
||||
|
||||
$$
|
||||
Z1 = \begin{pmatrix}
|
||||
z1_1 & z1_2 & z1_3
|
||||
\end{pmatrix}
|
||||
$$
|
||||
|
||||
$$
|
||||
A1 = \begin{pmatrix}
|
||||
a1_1 & a1_2 & a1_3
|
||||
\end{pmatrix}
|
||||
$$
|
||||
|
||||
|
||||
#### 权重矩阵W2/B2
|
||||
|
||||
W2的尺寸是3x1,B2的尺寸是1x1。
|
||||
|
||||
$$
|
||||
W2=
|
||||
\begin{pmatrix}
|
||||
w2_{11} \\\\
|
||||
w2_{21} \\\\
|
||||
w2_{31}
|
||||
\end{pmatrix}
|
||||
$$
|
||||
|
||||
$$
|
||||
B2=
|
||||
\begin{pmatrix}
|
||||
b2_{1}
|
||||
\end{pmatrix}
|
||||
$$
|
||||
|
||||
#### 输出层
|
||||
|
||||
由于我们只想完成一个拟合任务,所以输出层只有一个神经元,尺寸为1x1:
|
||||
|
||||
$$
|
||||
Z2 =
|
||||
\begin{pmatrix}
|
||||
z2_{1}
|
||||
\end{pmatrix}
|
||||
$$
|
||||
|
||||
### 9.4.3 前向计算
|
||||
|
||||
根据图9-7的网络结构,我们可以得到如图9-8的前向计算图。
|
||||
|
||||
<img src="./img/9/forward.png" />
|
||||
|
||||
图9-8 前向计算图
|
||||
|
||||
#### 隐层
|
||||
|
||||
- 线性计算
|
||||
|
||||
$$
|
||||
z1_{1} = x \cdot w1_{11} + b1_{1}
|
||||
$$
|
||||
|
||||
$$
|
||||
z1_{2} = x \cdot w1_{12} + b1_{2}
|
||||
$$
|
||||
|
||||
$$
|
||||
z1_{3} = x \cdot w1_{13} + b1_{3}
|
||||
$$
|
||||
|
||||
矩阵形式:
|
||||
|
||||
$$
|
||||
\begin{aligned}
|
||||
Z1 &=x \cdot
|
||||
\begin{pmatrix}
|
||||
w1_{11} & w1_{12} & w1_{13}
|
||||
\end{pmatrix}
|
||||
+
|
||||
\begin{pmatrix}
|
||||
b1_{1} & b1_{2} & b1_{3}
|
||||
\end{pmatrix}
|
||||
\\\\
|
||||
&= X \cdot W1 + B1
|
||||
\end{aligned} \tag{1}
|
||||
$$
|
||||
|
||||
- 激活函数
|
||||
|
||||
$$
|
||||
a1_{1} = Sigmoid(z1_{1})
|
||||
$$
|
||||
|
||||
$$
|
||||
a1_{2} = Sigmoid(z1_{2})
|
||||
$$
|
||||
|
||||
$$
|
||||
a1_{3} = Sigmoid(z1_{3})
|
||||
$$
|
||||
|
||||
矩阵形式:
|
||||
|
||||
$$
|
||||
A1 = Sigmoid(Z1) \tag{2}
|
||||
$$
|
||||
|
||||
#### 输出层
|
||||
|
||||
由于我们只想完成一个拟合任务,所以输出层只有一个神经元:
|
||||
|
||||
$$
|
||||
\begin{aligned}
|
||||
Z2&=a1_{1}w2_{11}+a1_{2}w2_{21}+a1_{3}w2_{31}+b2_{1} \\\\
|
||||
&=
|
||||
\begin{pmatrix}
|
||||
a1_{1} & a1_{2} & a1_{3}
|
||||
\end{pmatrix}
|
||||
\begin{pmatrix}
|
||||
w2_{11} \\\\ w2_{21} \\\\ w2_{31}
|
||||
\end{pmatrix}
|
||||
+b2_1 \\\\
|
||||
&=A1 \cdot W2+B2
|
||||
\end{aligned} \tag{3}
|
||||
$$
|
||||
|
||||
#### 损失函数
|
||||
|
||||
均方误差损失函数:
|
||||
|
||||
$$loss(w,b) = \frac{1}{2} (z2-y)^2 \tag{4}$$
|
||||
|
||||
其中,$z2$是预测值,$y$是样本的标签值。
|
||||
|
||||
### 9.4.4 反向传播
|
||||
|
||||
我们比较一下本章的神经网络和第5章的神经网络的区别,看表9-13。
|
||||
|
||||
表9-13 本章中的神经网络与第5章的神经网络的对比
|
||||
|
||||
|第5章的神经网络|本章的神经网络|
|
||||
|---|---|
|
||||
|<img src="..\第2步%20-%20线性回归\img\5\setup.png"/>|<img src="./img/9/nn.png"/>|
|
||||
|
||||
本章使用了真正的“网络”,而第5章充其量只是一个神经元而已。再看本章的网络的右半部分,从隐层到输出层的结构,和第5章的神经元结构一摸一样,只是输入为3个特征,而第5章的输入为两个特征。比较正向计算公式的话,也可以得到相同的结论。这就意味着反向传播的公式应该也是一样的。
|
||||
|
||||
由于我们第一次接触双层神经网络,所以需要推导一下反向传播的各个过程。看一下计算图,然后用链式求导法则反推。
|
||||
|
||||
#### 求损失函数对输出层的反向误差
|
||||
|
||||
根据公式4:
|
||||
|
||||
$$
|
||||
\frac{\partial loss}{\partial z2} = z2 - y \rightarrow dZ2 \tag{5}
|
||||
$$
|
||||
|
||||
#### 求W2的梯度
|
||||
|
||||
根据公式3和W2的矩阵形状,把标量对矩阵的求导分解到矩阵中的每一元素:
|
||||
|
||||
$$
|
||||
\begin{aligned}
|
||||
\frac{\partial loss}{\partial W2} &=
|
||||
\begin{pmatrix}
|
||||
\frac{\partial loss}{\partial z2}\frac{\partial z2}{\partial w2_{11}} \\\\
|
||||
\frac{\partial loss}{\partial z2}\frac{\partial z2}{\partial w2_{21}} \\\\
|
||||
\frac{\partial loss}{\partial z2}\frac{\partial z2}{\partial w2_{31}}
|
||||
\end{pmatrix}
|
||||
\begin{pmatrix}
|
||||
dZ2 \cdot a1_{1} \\\\
|
||||
dZ2 \cdot a1_{2} \\\\
|
||||
dZ2 \cdot a1_{3}
|
||||
\end{pmatrix} \\\\
|
||||
&=\begin{pmatrix}
|
||||
a1_{1} \\\\ a1_{2} \\\\ a1_{3}
|
||||
\end{pmatrix} \cdot dZ2
|
||||
=A1^{\top} \cdot dZ2 \rightarrow dW2
|
||||
\end{aligned} \tag{6}
|
||||
$$
|
||||
|
||||
#### 求B2的梯度
|
||||
|
||||
$$
|
||||
\frac{\partial loss}{\partial B2}=dZ2 \rightarrow dB2 \tag{7}
|
||||
$$
|
||||
|
||||
与第5章相比,除了把X换成A以外,其它的都一样。对于输出层来说,A就是它的输入,也就相当于是X。
|
||||
|
||||
#### 求损失函数对隐层的反向误差
|
||||
|
||||
下面的内容是双层神经网络独有的内容,也是深度神经网络的基础,请大家仔细阅读体会。我们先看看正向计算和反向计算图,即图9-9。
|
||||
|
||||
<img src="./img/9/backward.png" />
|
||||
|
||||
图9-9 正向计算和反向传播路径图
|
||||
|
||||
图9-9中:
|
||||
|
||||
- 蓝色矩形表示数值或矩阵;
|
||||
- 蓝色圆形表示计算单元;
|
||||
- 蓝色的箭头表示正向计算过程;
|
||||
- 红色的箭头表示反向计算过程。
|
||||
|
||||
如果想计算W1和B1的反向误差,必须先得到Z1的反向误差,再向上追溯,可以看到Z1->A1->Z2->Loss这条线,Z1->A1是一个激活函数的运算,比较特殊,所以我们先看Loss->Z->A1如何解决。
|
||||
|
||||
根据公式3和A1矩阵的形状:
|
||||
|
||||
$$
|
||||
\begin{aligned}
|
||||
\frac{\partial loss}{\partial A1}&=
|
||||
\begin{pmatrix}
|
||||
\frac{\partial loss}{\partial Z2}\frac{\partial Z2}{\partial a1_{11}}
|
||||
&
|
||||
\frac{\partial loss}{\partial Z2}\frac{\partial Z2}{\partial a1_{12}}
|
||||
&
|
||||
\frac{\partial loss}{\partial Z2}\frac{\partial Z2}{\partial a1_{13}}
|
||||
\end{pmatrix} \\\\
|
||||
&=
|
||||
\begin{pmatrix}
|
||||
dZ2 \cdot w2_{11} & dZ2 \cdot w2_{12} & dZ2 \cdot w2_{13}
|
||||
\end{pmatrix} \\\\
|
||||
&=dZ2 \cdot
|
||||
\begin{pmatrix}
|
||||
w2_{11} & w2_{21} & w2_{31}
|
||||
\end{pmatrix} \\\\
|
||||
&=dZ2 \cdot
|
||||
\begin{pmatrix}
|
||||
w2_{11} \\\\ w2_{21} \\\\ w2_{31}
|
||||
\end{pmatrix}^{\top}=dZ2 \cdot W2^{\top}
|
||||
\end{aligned} \tag{8}
|
||||
$$
|
||||
|
||||
现在来看激活函数的误差传播问题,由于公式2在计算时,并没有改变矩阵的形状,相当于做了一个矩阵内逐元素的计算,所以它的导数也应该是逐元素的计算,不改变误差矩阵的形状。根据Sigmoid激活函数的导数公式,有:
|
||||
|
||||
$$
|
||||
\frac{\partial A1}{\partial Z1}= Sigmoid'(A1) = A1 \odot (1-A1) \tag{9}
|
||||
$$
|
||||
|
||||
所以最后到达Z1的误差矩阵是:
|
||||
|
||||
$$
|
||||
\begin{aligned}
|
||||
\frac{\partial loss}{\partial Z1}&=\frac{\partial loss}{\partial A1}\frac{\partial A1}{\partial Z1} \\\\
|
||||
&=dZ2 \cdot W2^T \odot Sigmoid'(A1) \rightarrow dZ1
|
||||
\end{aligned} \tag{10}
|
||||
$$
|
||||
|
||||
有了dZ1后,再向前求W1和B1的误差,就和第5章中一样了,我们直接列在下面:
|
||||
|
||||
$$
|
||||
dW1=X^T \cdot dZ1 \tag{11}
|
||||
$$
|
||||
|
||||
$$
|
||||
dB1=dZ1 \tag{12}
|
||||
$$
|
||||
|
||||
### 9.4.5 代码实现
|
||||
|
||||
主要讲解神经网络`NeuralNet2`类的代码,其它的类都是辅助类。
|
||||
|
||||
#### 前向计算
|
||||
|
||||
```Python
|
||||
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`类型。
|
||||
|
||||
#### 反向传播
|
||||
```Python
|
||||
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结束后,都可以选择保存训练好的权重矩阵值,避免每次使用时重复训练浪费时间。
|
||||
|
||||
而在初始化完毕神经网络后,可以立刻加载历史权重矩阵数据(前提是本次的神经网络设置与保存时的一致),这样可以在历史数据的基础上继续训练,不会丢失以前的进度。
|
||||
|
||||
```Python
|
||||
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` - 超参类,各层的神经元数量、学习率、批大小、网络类型、初始化方法等
|
||||
|
||||
```Python
|
||||
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` - 权重矩阵类,初始化、加载数据、保存数据
|
||||
|
||||
### 代码位置
|
||||
|
||||
ch09, HelperClass2
|
||||
|
|
|
@ -3,147 +3,15 @@
|
|||
|
||||
## 9.5 曲线拟合
|
||||
|
||||
在上一节我们已经写好了神经网络的核心模块及其辅助功能,现在我们先来做一下正弦曲线的拟合,然后再试验复合函数的曲线拟合。
|
||||
以下为本小节目录,详情请参阅《智能之门》正版图书,高等教育出版社。
|
||||
|
||||
### 9.5.1 正弦曲线的拟合
|
||||
|
||||
#### 隐层只有一个神经元的情况
|
||||
|
||||
令`n_hidden=1`,并指定模型名称为`sin_111`,训练过程见图9-10。图9-11为拟合效果图。
|
||||
|
||||
<img src="./img/9/sin_loss_1n.png" />
|
||||
|
||||
图9-10 训练过程中损失函数值和准确率的变化
|
||||
|
||||
<img src="./img/9/sin_result_1n.png" ch="500" />
|
||||
|
||||
图9-11 一个神经元的拟合效果
|
||||
|
||||
从图9-10可以看到,损失值到0.04附近就很难下降了。图9-11中,可以看到只有中间线性部分拟合了,两端的曲线部分没有拟合。
|
||||
|
||||
```
|
||||
......
|
||||
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个神经元是基本不能工作的,这只比单层神经网络的线性拟合强一些,距离目标还差很远。
|
||||
|
||||
#### 隐层有两个神经元的情况
|
||||
|
||||
```Python
|
||||
if __name__ == '__main__':
|
||||
......
|
||||
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)
|
||||
......
|
||||
```
|
||||
|
||||
初始化神经网络类的参数有两个,第一个是超参组合`hp`,第二个是指定模型专有名称,以便把结果保存在名称对应的子目录中。保存训练结果的代码在训练结束后自动调用,但是如果想加载历史训练结果,需要在主过程中手动调用,比如上面代码中注释的那一行:`net.LoadResult()`。这样的话,如果下次再训练,就可以在以前的基础上继续训练,不必从头开始。
|
||||
|
||||
注意在主过程代码中,我们指定了n_hidden=2,意为隐层神经元数量为2。
|
||||
|
||||
#### 运行结果
|
||||
|
||||
图9-12为损失函数曲线和验证集精度曲线,都比较正常。而2个神经元的网络损失值可以达到0.004,少一个数量级。验证集精度到82%左右,而2个神经元的网络可以达到97%。图9-13为拟合效果图。
|
||||
|
||||
<img src="./img/9/sin_loss_2n.png"/>
|
||||
|
||||
图9-12 两个神经元的训练过程中损失函数值和准确率的变化
|
||||
|
||||
<img src="./img/9/sin_result_2n.png"/>
|
||||
|
||||
图9-13 两个神经元的拟合效果
|
||||
|
||||
再看下面的打印输出结果,最后测试集的精度为98.8%。如果需要精度更高的话,可以增加迭代次数。
|
||||
|
||||
```
|
||||
......
|
||||
epoch=4999, total_iteration=224999
|
||||
loss_train=0.007681, accuracy_train=0.971567
|
||||
loss_valid=0.004366, accuracy_valid=0.979845
|
||||
testing...
|
||||
0.9881468747638157
|
||||
```
|
||||
|
||||
### 9.5.2 复合函数的拟合
|
||||
|
||||
基本过程与正弦曲线相似,区别是这个例子要复杂不少,所以首先需要耐心,增大`max_epoch`的数值,多迭代几次。其次需要精心调参,找到最佳参数组合。
|
||||
|
||||
#### 隐层只有两个神经元的情况
|
||||
|
||||
<img src="./img/9/complex_result_2n.png" ch="500" />
|
||||
|
||||
图9-14 两个神经元的拟合效果
|
||||
|
||||
图9-14是两个神经元的拟合效果图,拟合情况很不理想,和正弦曲线只用一个神经元的情况类似。观察打印输出的损失值,有波动,久久徘徊在0.003附近不能下降,说明网络能力不够。
|
||||
|
||||
```
|
||||
epoch=99999, total_iteration=8999999
|
||||
loss_train=0.000751, accuracy_train=0.968484
|
||||
loss_valid=0.003200, accuracy_valid=0.795622
|
||||
testing...
|
||||
0.8641114405898856
|
||||
```
|
||||
|
||||
#### 隐层有三个神经元的情况
|
||||
|
||||
```Python
|
||||
if __name__ == '__main__':
|
||||
......
|
||||
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")
|
||||
......
|
||||
```
|
||||
|
||||
#### 运行结果
|
||||
|
||||
图9-15为损失函数曲线和验证集精度曲线,都比较正常。图9-16是拟合效果。
|
||||
|
||||
<img src="./img/9/complex_loss_3n.png" />
|
||||
|
||||
图9-15 三个神经元的训练过程中损失函数值和准确率的变化
|
||||
|
||||
<img src="./img/9/complex_result_3n.png"/>
|
||||
|
||||
图9-16 三个神经元的拟合效果
|
||||
|
||||
再看下面的打印输出结果,最后测试集的精度为97.6%,已经令人比较满意了。如果需要精度更高的话,可以增加迭代次数。
|
||||
|
||||
```
|
||||
......
|
||||
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个神经元
|
||||
- 学习率=0.5
|
||||
- 批量=10
|
||||
|
||||
### 9.5.3 广义的回归/拟合
|
||||
|
||||
至此我们用两个可视化的例子完成了曲线拟合,验证了万能近似定理。但是,神经网络不是设计专门用于曲线拟合的,这只是牛刀小试而已,我们用简单的例子讲解了神经网络的功能,但是此功能完全可以用于多变量的复杂非线性回归。
|
||||
|
||||
“曲线”在这里是一个广义的概念,它不仅可以代表二维平面上的数学曲线,也可以代表工程实践中的任何拟合问题,比如房价预测问题,影响房价的自变量可以达到20个左右,显然已经超出了线性回归的范畴,此时我们可以用多层神经网络来做预测。在后面我们会讲解这样的例子。
|
||||
|
||||
简言之,只要是数值拟合问题,确定不能用线性回归的话,都可以用非线性回归来尝试解决。
|
||||
|
||||
### 代码位置
|
||||
|
||||
ch09, Level3, Level4
|
||||
|
|
|
@ -3,219 +3,15 @@
|
|||
|
||||
## 9.6 非线性回归的工作原理
|
||||
|
||||
以下为本小节目录,详情请参阅《智能之门》正版图书,高等教育出版社。
|
||||
|
||||
### 9.6.1 多项式为何能拟合曲线
|
||||
|
||||
先回忆一下本章最开始讲的多项式回归法,它成功地用于正弦曲线和复合函数曲线的拟合,其基本工作原理是把单一特征值的高次方做为额外的特征值加入,使得神经网络可以得到附加的信息用于训练。实践证明其方法有效,但是当问题比较复杂时,需要高达8次方的附加信息,且训练时间也很长。
|
||||
|
||||
当我们使用双层神经网络时,在隐层只放置了三个神经元,就轻松解决了复合函数拟合的问题,效率高出十几倍,复杂度却降低了几倍。那么含有隐层的神经网络究竟是如何完成这个任务的呢?
|
||||
|
||||
我们以正弦曲线拟合为例来说明这个问题,首先看一下多项式回归方法的示意图,如图9-17。
|
||||
|
||||
<img src="./img/9/polynomial_concept.png"/>
|
||||
|
||||
图9-17 多项式回归方法的特征值输入
|
||||
|
||||
单层神经网络的多项式回归法,需要$x,x^2,x^3$三个特征值,组成如下公式来得到拟合结果:
|
||||
|
||||
$$
|
||||
z = x \cdot w_1 + x^2 \cdot w_2 + x^3 \cdot w_3 + b \tag{1}
|
||||
$$
|
||||
|
||||
我们可以回忆一下第5章学习的多变量线性回归问题,公式1实际上是把一维的x的特征信息,增加到了三维,然后再使用多变量线性回归来解决问题的。本来一维的特征只能得到线性的结果,但是三维的特征就可以得到非线性的结果,这就是多项式拟合的原理。
|
||||
|
||||
我们用具体的数值计算方式来理解一下其工作过程。
|
||||
|
||||
```Python
|
||||
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. 绘制出三条曲线,如图9-18所示。
|
||||
|
||||
<img src="./img/9/concept1.png" ch="500" />
|
||||
|
||||
图9-18 线性到非线性的变换
|
||||
|
||||
可以清楚地看到:
|
||||
|
||||
- 蓝色直线是线性回归数值序列;
|
||||
- 红色曲线是二项式回归数值序列;
|
||||
- 绿色曲线是三项式回归数值序列。
|
||||
|
||||
也就是说,我们只使用了同一个x序列的原始值,却可以得到三种不同数值序列,这就是多项式拟合的原理。当多项式次数很高、数据样本充裕、训练足够多的时候,甚至可以拟合出非单调的曲线。
|
||||
|
||||
### 9.6.2 神经网络的非线性拟合工作原理
|
||||
|
||||
我们以正弦曲线的例子来讲解神经网络非线性回归的工作过程和原理。
|
||||
|
||||
表9-14
|
||||
|
||||
|单层多项式回归|双层神经网络|
|
||||
|---|---|
|
||||
|<img src="./img/9/polynomial_concept.png">|<img src="./img/9/neuralnet_concept.png">|
|
||||
|
||||
比较一下表9-14中的两张图,左侧为单特征多项式拟合的示意图,右侧为双层神经网络的示意图。
|
||||
|
||||
左图中,通过人为的方式,给Z的输入增加了$x^2和x^3$项。
|
||||
|
||||
右图中,通过线性变换的方式,把x变成了两部分:$z_{11}/a_{11},z_{12}/a_{12}$,然后再通过一次线性变换把两者组合成为Z,这种方式和多项式回归非常类似:
|
||||
|
||||
1. 隐层把x拆成不同的特征,根据问题复杂度决定神经元数量,神经元的数量相当于特征值的数量;
|
||||
2. 隐层通过激活函数做一次非线性变换;
|
||||
3. 输出层使用多变量线性回归,把隐层的输出当作输入特征值,再做一次线性变换,得出拟合结果。
|
||||
|
||||
与多项式回归不同的是,不需要指定变换参数,而是从训练中学习到参数,这样的话权重值不会大得离谱。
|
||||
|
||||
下面讲述具体的工作步骤。
|
||||
|
||||
#### 第一步 把X拆成两个线性序列z1和z2
|
||||
|
||||
假设原始值x有21个点,样本数据如表9-15所示。
|
||||
|
||||
表9-15
|
||||
|
||||
|id|0|1|2|...|19|20|21|
|
||||
|--|--|--|--|--|--|--|--|
|
||||
|x|0.|0.05|0.1|...|0.9|0.95|1.|
|
||||
|
||||
通过以下线性变换,被分成了两个线性序列,得到表9-16所示的隐层值:
|
||||
|
||||
$$
|
||||
z1 = x \cdot w_{11} + b_{11} \tag{2}
|
||||
$$
|
||||
$$
|
||||
z2 = x \cdot w_{12} + b_{12} \tag{3}
|
||||
$$
|
||||
|
||||
其中:
|
||||
|
||||
- $w_{11} = -2.673$
|
||||
- $b_{11} = 1.303$
|
||||
- $w_{12} = -9.036$
|
||||
- $b_{12} = 4.507$
|
||||
|
||||
表9-16 隐层线性变化结果
|
||||
|
||||
||0|1|2|...|19|20|21|
|
||||
|--|--|--|--|--|--|--|--|
|
||||
|z1|1.303|1.169|1.035|...|-1.102|-1.236|-1.369|
|
||||
|z2|4.507|4.055|3.603|...|-3.625|-4.077|-4.528|
|
||||
|
||||
三个线性序列如图9-19所示,黑色点是原始数据序列,红色和绿色点是拆分后的两个序列。
|
||||
|
||||
<img src="./img/9/nn_concept_x_z1_z2.png" ch="500" />
|
||||
|
||||
图9-19 从原始数据序列拆分成的两个数据序列
|
||||
|
||||
这个运算相当于把特征值分解成两个部分,不太容易理解。打个不太恰当的比喻,有一个浮点数12.34,你可以把它拆成12和0.34两个部分,然后去分别做一些运算。另外一个例子就是,一张彩色图片上的黄色,我们普通人看到的就是黄色,但是画家会想到是红色和绿色的组合。
|
||||
|
||||
#### 第二步 计算z1的激活函数值a1
|
||||
|
||||
表9-17和图9-20分别展示了隐层对第一个特征值的计算结果数值和示意图。
|
||||
|
||||
表9-17 第一个特征值及其激活函数结果数值
|
||||
|
||||
||0|1|2|...|19|20|21|
|
||||
|--|--|--|--|--|--|--|--|
|
||||
|z1|1.303|1.169|1.035|...|-1.102|-1.236|-1.369|
|
||||
|a1|0.786|0.763|0.738|...|0.249|0.225|0.203|
|
||||
|
||||
第二行的a1值等于第1行的z1值的sigmoid函数值:
|
||||
|
||||
$$a1 = {1 \over 1+e^{-z1}} \tag{4}$$
|
||||
|
||||
<img src="./img/9/nn_concept_x_z1_a1.png" ch="500" />
|
||||
|
||||
图9-20 第一个特征值及其激活函数结果可视化
|
||||
|
||||
z1还是一条直线,但是经过激活函数后的a1已经不是一条直线了。上面这张图由于z1的跨度大,所以a1的曲线程度不容易看出来。
|
||||
|
||||
#### 第三步 计算z2的激活函数值a2
|
||||
|
||||
表9-18和图9-21分别展示了隐层对第二个特征值的计算结果数值和示意图。
|
||||
|
||||
表9-18 第二个特征值及其激活函数结果数值
|
||||
|
||||
||0|1|2|...|19|20|21|
|
||||
|--|--|--|--|--|--|--|--|
|
||||
|z2|4.507|4.055|3.603|...|-3.625|-4.077|-4.528|
|
||||
|a2|0.989|0.983|0.973|...|0.026|0.017|0.011|
|
||||
|
||||
$$a2 = {1 \over 1+e^{-z2}} \tag{5}$$
|
||||
|
||||
<img src="./img/9/nn_concept_x_z2_a2.png" ch="500" />
|
||||
|
||||
图9-21 第二个特征值及其激活函数结果可视化
|
||||
|
||||
z2还是一条直线,但是经过激活函数后的a2已经明显看出是一条曲线了。
|
||||
|
||||
#### 第四步 计算Z值
|
||||
|
||||
表9-19和图9-22分别展示了输出层对两个特征值的计算结果数值和示意图。
|
||||
|
||||
表9-19 输出层的计算结果数值
|
||||
|
||||
||0|1|2|...|19|20|21|
|
||||
|--|--|--|--|--|--|--|--|
|
||||
|a1|0.786|0.763|0.738|...|0.249|0.225|0.203|
|
||||
|a2|0.989|0.983|0.973|...|0.026|0.017|0.011|
|
||||
|z|0.202|0.383|0.561|...|-0.580|-0.409|-0.235|
|
||||
|
||||
$$z = a1 \cdot w_{11} + a2 \cdot w_{21} + b \tag{6}$$
|
||||
|
||||
其中:
|
||||
|
||||
- $w_{11}=-9.374$
|
||||
- $w_{21}=6.039$
|
||||
- $b=1.599$
|
||||
|
||||
<img src="./img/9/nn_concept_a1_a2_z.png" ch="500" />
|
||||
|
||||
图9-22 输出层的计算结果可视化
|
||||
|
||||
也就是说,相同x值的红点a1和绿点a2,经过公式6计算后得到蓝点z,而所有的蓝点就拟合出一条正弦曲线。
|
||||
|
||||
### 9.6.3 比较多项式回归和双层神经网络解法
|
||||
|
||||
表9-20列出了多项式回归和神经网络的比较结果,可以看到神经网络处于绝对的优势地位。
|
||||
|
||||
表9-20 多项式回归和神经网络的比较
|
||||
|
||||
||多项式回归|双层神经网络|
|
||||
|---|---|---|
|
||||
|特征提取方式|特征值的高次方|线性变换拆分|
|
||||
|特征值数量级|高几倍的数量级|数量级与原特征值相同|
|
||||
|训练效率|低,需要迭代次数多|高,比前者少好几个数量级|
|
||||
|
||||
### 代码位置
|
||||
|
||||
|
|
|
@ -3,206 +3,16 @@
|
|||
|
||||
## 9.7 超参数优化的初步认识
|
||||
|
||||
超参数优化(Hyperparameter Optimization)主要存在两方面的困难:
|
||||
|
||||
1. 超参数优化是一个组合优化问题,无法像一般参数那样通过梯度下降方法来优化,也没有一种通用有效的优化方法。
|
||||
2. 评估一组超参数配置(Configuration)的时间代价非常高,从而导致一些优化方法(比如演化算法)在超参数优化中难以应用。
|
||||
|
||||
对于超参数的设置,比较简单的方法有人工搜索、网格搜索和随机搜索。
|
||||
以下为本小节目录,详情请参阅《智能之门》正版图书,高等教育出版社。
|
||||
|
||||
### 9.7.1 可调的参数
|
||||
|
||||
我们使用表9-21所示的参数做第一次的训练。
|
||||
|
||||
表9-21 参数配置
|
||||
|
||||
|参数|缺省值|是否可调|注释|
|
||||
|---|---|---|---|
|
||||
|输入层神经元数|1|No|
|
||||
|隐层神经元数|4|Yes|影响迭代次数|
|
||||
|输出层神经元数|1|No|
|
||||
|学习率|0.1|Yes|影响迭代次数|
|
||||
|批样本量|10|Yes|影响迭代次数|
|
||||
|最大epoch|10000|Yes|影响终止条件,建议不改动|
|
||||
|损失门限值|0.001|Yes|影响终止条件,建议不改动|
|
||||
|损失函数|MSE|No|
|
||||
|权重矩阵初始化方法|Xavier|Yes|参看15.1|
|
||||
|
||||
表9-21中的参数,最终可以调节的其实只有三个:
|
||||
|
||||
- 隐层神经元数
|
||||
- 学习率
|
||||
- 批样本量
|
||||
|
||||
另外还有一个权重矩阵初始化方法需要特别注意,我们在后面的章节中讲解。
|
||||
|
||||
另外两个要提一下的参数,第一个是最大epoch数,根据不同的模型和案例会有所不同,在本例中10000次足以承载所有的超参组合了;另外一个是损失门限值,它是一个先验数值,也就是说笔者通过试验,事先知道了当`eps=0.001`时,会训练出精度可接受的模型来,但是在实践中没有这种先验知识,只能摸着石头过河,因为在训练一个特定模型之前,谁也不能假设它能到达的精度值是多少,损失函数值的下限也是通过多次试验,通过历史记录的趋势来估算出来的。
|
||||
|
||||
如果读者不了解神经网络中的基本原理,那么所谓“调参”就是碰运气了。今天咱们可以试着改变几个参数,来看看训练结果,以此来增加对神经网络中各种参数的了解。
|
||||
|
||||
#### 避免权重矩阵初始化的影响
|
||||
|
||||
权重矩阵中的参数,是神经网络要学习的参数,所以不能称作超参数。
|
||||
|
||||
权重矩阵初始化是神经网络训练非常重要的环节之一,不同的初始化方法,甚至是相同的方法但不同的随机值,都会给结果带来或多或少的影响。
|
||||
|
||||
在后面的几组比较中,都是用Xavier方法初始化的。在两次参数完全相同的试验中,即使两次都使用Xavier初始化,因为权重矩阵参数的差异,也会得到不同的结果。为了避免这个随机性,我们在代码`WeightsBias.py`中使用了一个小技巧,调用下面这个函数:
|
||||
|
||||
```Python
|
||||
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`,只要隐层神经元数量不变并且初始化方法不变,就会用第一次的初始化结果,否则后面的各种参数调整的结果就没有可比性了。
|
||||
|
||||
至于为什么使用Xavier方法初始化,将在15.1中讲解。
|
||||
|
||||
### 9.7.2 手动调整参数
|
||||
|
||||
手动调整超参数,我们必须了解超参数、训练误差、泛化误差和计算资源(内存和运行时间)之间的关系。手动调整超参数的主要目标是调整模型的有效容量以匹配任务的复杂性。有效容量受限于3个因素:
|
||||
|
||||
- 模型的表示容量;
|
||||
- 学习算法与代价函数的匹配程度;
|
||||
- 代价函数和训练过程正则化模型的程度。
|
||||
|
||||
表9-22比较了几个超参数的作用。具有更多网络层、每层有更多隐藏单元的模型具有较高的表示能力,能够表示更复杂的函数。学习率是最重要的超参数。如果你只有一个超参数调整的机会,那就调整学习率。
|
||||
|
||||
表9-22 各种超参数的作用
|
||||
|
||||
|超参数|目标|作用|副作用|
|
||||
|---|---|---|---|
|
||||
|学习率|调至最优|低的学习率会导致收敛慢,高的学习率会导致错失最佳解|容易忽略其它参数的调整|
|
||||
|隐层神经元数量|增加|增加数量会增加模型的表示能力|参数增多、训练时间增长|
|
||||
|批大小|有限范围内尽量大|大批量的数据可以保持训练平稳,缩短训练时间|可能会收敛速度慢|
|
||||
|
||||
通常的做法是,按经验设置好隐层神经元数量和批大小,并使之相对固定,然后调整学习率。
|
||||
|
||||
### 9.7.3 网格搜索
|
||||
|
||||
当有3个或更少的超参数时,常见的超参数搜索方法是网格搜索(grid search)。对于每个超参数,选择一个较小的有限值集去试验。然后,这些超参数的笛卡儿乘积(所有的排列组合)得到若干组超参数,网格搜索使用每组超参数训练模型。挑选验证集误差最小的超参数作为最好的超参数组合。
|
||||
|
||||
用学习率和隐层神经元数量来举例,横向为学习率,取值 $[0.1,0.3,0.5,0.7]$;纵向为隐层神经元数量,取值 $[2,4,8,12]$,在每个组合上测试验证集的精度。我们假设其中最佳的组合精度达到0.97,学习率为0.5,神经元数为8,那么这个组合就是我们需要的模型超参,可以拿到测试集上去做最终测试了。
|
||||
|
||||
表9-23数据为假设的结果值,用于说明如何选择最终参数值。
|
||||
|
||||
表9-23 各种组合下的准确率
|
||||
|
||||
||eta=0.1|eta=0.3|eta=0.5|eta=0.7|
|
||||
|---|---|---|---|---|
|
||||
|ne=2|0.63|0.68|0.71|0.73|
|
||||
|ne=4|0.86|0.89|0.91|0.3|
|
||||
|ne=8|0.92|0.94|0.97|0.95|
|
||||
|ne=12|0.69|0.84|0.88|0.87|
|
||||
|
||||
针对我们这个曲线拟合问题,规模较小,模型简单,所以可以用上表列出的数据做搜索。对于大规模模型问题,学习率的取值集合可以是 $\\{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`为最低精度要求,一旦到达,就停止训练。
|
||||
|
||||
表9-24和图9-23展示了四种学习率值的不同结果。
|
||||
|
||||
表9-24 四种学习率值的比较
|
||||
|
||||
|学习率|迭代次数|说明|
|
||||
|----|----|----|
|
||||
|0.1|10000|学习率小,收敛最慢,没有在规定的次数内达到精度要求|
|
||||
|0.3|10000|学习率增大,收敛慢,没有在规定的次数内达到精度要求|
|
||||
|0.5|8200|学习率增大,在8200次左右达到精度要求|
|
||||
|0.7|3500|学习率进一步增大,在3500次达到精度|
|
||||
|
||||
<img src="./img/9/eta.png" ch="500" />
|
||||
|
||||
图9-23 四种学习率值造成的损失函数值的变化
|
||||
|
||||
需要说明的是,对于本例的拟合曲线这个特定问题,较大的学习率可以带来很快的收敛速度,但是有两点:
|
||||
|
||||
- 但并不是对所有问题都这样,有的问题可能需要0.001或者更小的学习率
|
||||
- 学习率大时,开始时收敛快,但是到了后来有可能会错失最佳解
|
||||
|
||||
#### 批大小的调整
|
||||
|
||||
我们固定其它参数,即隐层神经元`ne=4`、`eta=0.5`不变,调整批大小,来试验网络训练情况,设置`eps=0.001`为精度要求。
|
||||
|
||||
表9-25和图9-24展示了四种批大小值的不同结果。
|
||||
|
||||
表9-25 四种批大小数值的比较
|
||||
|
||||
|批大小|迭代次数|说明|
|
||||
|----|----|----|
|
||||
|5|2500|批数据量小到1,收敛最快|
|
||||
|10|8200|批数据量增大,收敛变慢|
|
||||
|15|10000|批数据量进一步增大,收敛变慢|
|
||||
|20|10000|批数据量太大,反而会降低收敛速度|
|
||||
|
||||
<img src="./img/9/batchsize.png" ch="500" />
|
||||
|
||||
图9-24 四种批大小值造成的损失函数值的变化
|
||||
|
||||
合适的批样本量会带来较快的收敛,前提是我们固定了学习率。如果想用较大的批数据,底层数据库计算的速度较快,但是需要同时调整学习率,才会相应地提高收敛速度。
|
||||
|
||||
这个结论的前提是我们用了0.5的学习率,如果用0.1的话,将会得到不同结论。
|
||||
|
||||
#### 隐层神经元数量的调整
|
||||
|
||||
我们固定其它参数,即`batch_size=10`、`eta=0.5`不变,调整隐层神经元的数量,来试验网络训练情况,设置`eps=0.001`为精度要求。
|
||||
|
||||
表9-26和图9-25展示了四种神经元数值的不同结果。
|
||||
|
||||
表9-26 四种隐层神经元数量值的比较
|
||||
|
||||
|隐层神经元数量|迭代次数|说明|
|
||||
|---|---|---|
|
||||
|2|10000|神经元数量少,拟合能力低|
|
||||
|4|8000|神经元数量增加会有帮助|
|
||||
|6|5500|神经元数量进一步增加,收敛更快|
|
||||
|8|3500|再多一些神经元,还会继续提供收敛速度|
|
||||
|
||||
<img src="./img/9/neuron_number.png" ch="500" />
|
||||
|
||||
图9-25 四种隐层神经元数量值造成的损失函数值的变化
|
||||
|
||||
对于这个特定问题,隐层神经元个数越多,收敛速度越快。但实际上,这个比较不准确,因为隐层神经元数量的变化,会导致权重矩阵的尺寸变化,因此对于上述4种试验,权重矩阵的初始值都不一样,不具有很强的可比性。我们只需要明白神经元数量多可以提高网络的学习能力这一点就可以了。
|
||||
|
||||
### 9.7.4 随机搜索
|
||||
|
||||
随机搜索(Bergstra and Bengio,2012),是一个替代网格搜索的方法,并且编程简单,使用更方便,能更快地收敛到超参数的良好取值。
|
||||
|
||||
随机搜索过程如下:
|
||||
|
||||
首先,我们为每个超参 数定义一个边缘分布,例如,Bernoulli分布或范畴分布(分别对应着二元超参数或离散超参数),或者对数尺度上的均匀分布(对应着正实 值超参数)。例如,其中,$U(a,b)$ 表示区间$(a,b)$ 上均匀采样的样本。类似地,`log_number_of_hidden_units`可以从 $U(\ln(50),\ln(2000))$ 上采样。
|
||||
|
||||
与网格搜索不同,我们不需要离散化超参数的值。这允许我们在一个更大的集合上进行搜索,而不产生额外的计算代价。实际上,当有几个超参数对性能度量没有显著影响时,随机搜索相比于网格搜索指数级地高效。
|
||||
|
||||
Bergstra and Bengio(2012)进行了详细的研究并发现相比于网格搜索,随机搜索能够更快地减小验证集误差(就每个模型运行的试验数而 言)。
|
||||
|
||||
与网格搜索一样,我们通常会重复运行不同 版本的随机搜索,以基于前一次运行的结果改进下一次搜索。
|
||||
|
||||
随机搜索能比网格搜索更快地找到良好超参数的原因是,没有浪费的实验,不像网格搜索有时会对一个超参数的两个不同值(给定其他超参 数值不变)给出相同结果。在网格搜索中,其他超参数将在这两次实验中拥有相同的值,而在随机搜索中,它们通常会具有不同的值。因此,如果这两个值的变化所对应的验证集误差没有明显区别的话,网格搜索没有必要重复两个等价的实验,而随机搜索仍然会对其他超参数进行两次独立的探索。
|
||||
|
||||
贝叶斯优化是另外一种比较成熟技术,有兴趣的读者请自行学习。
|
||||
|
||||
### 思考与练习
|
||||
|
||||
|
|
|
@ -3,107 +3,13 @@
|
|||
|
||||
## 10.1 为什么必须用双层神经网络
|
||||
|
||||
以下为本小节目录,详情请参阅《智能之门》正版图书,高等教育出版社。
|
||||
|
||||
### 10.1.1 分类
|
||||
|
||||
我们先回忆一下各种分类的含义:
|
||||
|
||||
- 从复杂程度上分,有线性/非线性之分;
|
||||
- 从样本类别上分,有二分类/多分类之分。
|
||||
|
||||
从直观上理解,这几个概念应该符合表10-2中的示例。
|
||||
|
||||
表10-2 各种分类的组合关系
|
||||
|
||||
||二分类|多分类|
|
||||
|---|---|---|
|
||||
|线性|<img src="./img/6/linear_binary.png"/>|<img src="./img/6/linear_multiple.png"/>|
|
||||
|非线性|<img src="./img/10/non_linear_binary.png"/>|<img src="./img/10/non_linear_multiple.png"/>|
|
||||
|
||||
在第三步中我们学习过线性分类,如果用于此处的话,我们可能会得到表10-3所示的绿色分割线。
|
||||
|
||||
表10-3 线性分类结果
|
||||
|
||||
|XOR问题|弧形问题|
|
||||
|---|---|
|
||||
|<img src='./img/10/xor_data_line.png'/>|<img src='./img/10/sin_data_line.png'/>|
|
||||
|图中两根直线中的任何一根,都不可能把蓝色点分到一侧,同时红色点在另一侧|对于线性技术来说,它已经尽力了,使得两类样本尽可能地分布在直线的两侧|
|
||||
|
||||
### 10.1.2 简单证明异或问题的不可能性
|
||||
|
||||
用单个感知机或者单层神经网络,是否能完成异或任务呢?我们自己做个简单的证明。先看样本数据,如表10-4。
|
||||
|
||||
表10-4 异或的样本数据
|
||||
|
||||
|样本|$x_1$|$x_2$|$y$|
|
||||
|---|---|---|---|
|
||||
|1|0|0|0|
|
||||
|2|0|1|1|
|
||||
|3|1|0|1|
|
||||
|4|1|1|0|
|
||||
|
||||
用单个神经元(感知机)的话,就是表10-5中两种技术的组合。
|
||||
|
||||
表10-5 神经元结构与二分类函数
|
||||
|
||||
|神经元|分类函数Logistic|
|
||||
|--|--|
|
||||
|<img src='./img/10/xor_prove.png' width="400"/>|<img src='../第4步%20-%20非线性回归/img/8/sigmoid_seperator.png' width="430"/>|
|
||||
|
||||
前向计算公式:
|
||||
|
||||
$$z = x_1 w_1 + x_2 w_2 + b \tag{1}$$
|
||||
$$a = Logistic(z) \tag{2}$$
|
||||
|
||||
- 对于第一个样本数据
|
||||
|
||||
$x_1=0,x_2=0,y=0$。如果需要$a=y$的话,从Logistic函数曲线看,需要$z<0$,于是有:
|
||||
|
||||
$$x_1 w_1 + x_2 w_2 + b < 0$$
|
||||
|
||||
因为$x_1=0,x_2=0$,所以只剩下$b$项:
|
||||
|
||||
$$b < 0 \tag{3}$$
|
||||
|
||||
- 对于第二个样本数据
|
||||
|
||||
$x_1=0,x_2=1,y=1$。如果需要$a=y$,则要求$z>0$,不等式为:
|
||||
|
||||
$$x_1w_1 + x_2w_2+b=w_2+b > 0 \tag{4}$$
|
||||
|
||||
- 对于第三个样本数据
|
||||
|
||||
$x_1=1,x_2=0,y=1$。如果需要$a=y$,则要求$z>0$,不等式为:
|
||||
|
||||
$$x_1w_1 + x_2w_2+b=w_1+b > 0 \tag{5}$$
|
||||
|
||||
- 对于第四个样本
|
||||
|
||||
$x_1=1,x_2=1,y=0$。如果需要$a=y$,则要求$z<0$,不等式为:
|
||||
|
||||
$$x_1w_1 + x_2w_2+b=w_1+w_2+b < 0 \tag{6}$$
|
||||
|
||||
把公式6两边都加$b$,并把公式3接续:
|
||||
|
||||
$$(w_1 + b) + (w_2 + b) < b < 0 \tag{7}$$
|
||||
|
||||
再看公式4、5,不等式左侧括号内的两个因子都大于0,其和必然也大于0,不可能小于$b$。因此公式7不成立,无论如何也不能满足所有的4个样本的条件,所以单个神经元做异或运算是不可能的。
|
||||
|
||||
### 10.1.3 非线性的可能性
|
||||
|
||||
我们前边学习过如何实现与、与非、或、或非,我们看看如何用已有的逻辑搭建异或门,如图10-5所示。
|
||||
|
||||
<img src="./img/10/xor_gate.png" />
|
||||
|
||||
图10-5 用基本逻辑单元搭建异或运算单元
|
||||
|
||||
表10-6 组合运算的过程
|
||||
|
||||
|样本与计算|1|2|3|4|
|
||||
|----|----|----|----|----|
|
||||
|$x_1$|0|0|1|1|
|
||||
|$x_2$|0|1|0|1|
|
||||
|$s_1=x_1$ NAND $x_2$|1|1|1|0|
|
||||
|$s_2=x_1$ OR $x_2$|0|1|1|1|
|
||||
|$y=s_1$ AND $s_2$|0|1|1|0|
|
||||
|
||||
经过表10-6所示的组合运算后,可以看到$y$的输出与$x_1,x_2$的输入相比,就是异或逻辑了。所以,实践证明两层逻辑电路可以解决问题。另外,我们在地四步中学习了非线性回归,使用双层神经网络可以完成一些神奇的事情,比如复杂曲线的拟合,只需要6、7个参数就搞定了。我们可以模拟这个思路,用两层神经网络搭建模型,来解决非线性分类问题。
|
||||
|
|
|
@ -3,239 +3,10 @@
|
|||
|
||||
## 10.2 非线性二分类实现
|
||||
|
||||
以下为本小节目录,详情请参阅《智能之门》正版图书,高等教育出版社。
|
||||
|
||||
### 10.2.1 定义神经网络结构
|
||||
|
||||
首先定义可以完成非线性二分类的神经网络结构图,如图10-6所示。
|
||||
|
||||
<img src="./img/10/xor_nn.png" />
|
||||
|
||||
图10-6 非线性二分类神经网络结构图
|
||||
|
||||
- 输入层两个特征值$x_1,x_2$
|
||||
$$
|
||||
X=\begin{pmatrix}
|
||||
x_1 & x_2
|
||||
\end{pmatrix}
|
||||
$$
|
||||
- 隐层$2\times 2$的权重矩阵$W1$
|
||||
$$
|
||||
W1=\begin{pmatrix}
|
||||
w1_{11} & w1_{12} \\\\
|
||||
w1_{21} & w1_{22}
|
||||
\end{pmatrix}
|
||||
$$
|
||||
- 隐层$1\times 2$的偏移矩阵$B1$
|
||||
|
||||
$$
|
||||
B1=\begin{pmatrix}
|
||||
b1_{1} & b1_{2}
|
||||
\end{pmatrix}
|
||||
$$
|
||||
|
||||
- 隐层由两个神经元构成
|
||||
$$
|
||||
Z1=\begin{pmatrix}
|
||||
z1_{1} & z1_{2}
|
||||
\end{pmatrix}
|
||||
$$
|
||||
$$
|
||||
A1=\begin{pmatrix}
|
||||
a1_{1} & a1_{2}
|
||||
\end{pmatrix}
|
||||
$$
|
||||
- 输出层$2\times 1$的权重矩阵$W2$
|
||||
$$
|
||||
W2=\begin{pmatrix}
|
||||
w2_{11} \\\\
|
||||
w2_{21}
|
||||
\end{pmatrix}
|
||||
$$
|
||||
|
||||
- 输出层$1\times 1$的偏移矩阵$B2$
|
||||
|
||||
$$
|
||||
B2=\begin{pmatrix}
|
||||
b2_{1}
|
||||
\end{pmatrix}
|
||||
$$
|
||||
|
||||
- 输出层有一个神经元使用Logistic函数进行分类
|
||||
$$
|
||||
Z2=\begin{pmatrix}
|
||||
z2_{1}
|
||||
\end{pmatrix}
|
||||
$$
|
||||
$$
|
||||
A2=\begin{pmatrix}
|
||||
a2_{1}
|
||||
\end{pmatrix}
|
||||
$$
|
||||
|
||||
对于一般的用于二分类的双层神经网络可以是图10-7的样子。
|
||||
|
||||
<img src="./img/10/binary_classifier.png" width="600" ch="500" />
|
||||
|
||||
图10-7 通用的二分类神经网络结构图
|
||||
|
||||
输入特征值可以有很多,隐层单元也可以有很多,输出单元只有一个,且后面要接Logistic分类函数和二分类交叉熵损失函数。
|
||||
|
||||
### 10.2.2 前向计算
|
||||
|
||||
根据网络结构,我们有了前向计算过程图10-8。
|
||||
|
||||
<img src="./img/10/binary_forward.png" />
|
||||
|
||||
图10-8 前向计算过程
|
||||
|
||||
#### 第一层
|
||||
|
||||
- 线性计算
|
||||
|
||||
$$
|
||||
z1_{1} = x_{1} w1_{11} + x_{2} w1_{21} + b1_{1}
|
||||
$$
|
||||
$$
|
||||
z1_{2} = x_{1} w1_{12} + x_{2} w1_{22} + b1_{2}
|
||||
$$
|
||||
$$
|
||||
Z1 = X \cdot W1 + B1
|
||||
$$
|
||||
|
||||
- 激活函数
|
||||
|
||||
$$
|
||||
a1_{1} = Sigmoid(z1_{1})
|
||||
$$
|
||||
$$
|
||||
a1_{2} = Sigmoid(z1_{2})
|
||||
$$
|
||||
$$
|
||||
A1=\begin{pmatrix}
|
||||
a1_{1} & a1_{2}
|
||||
\end{pmatrix}=Sigmoid(Z1)
|
||||
$$
|
||||
|
||||
#### 第二层
|
||||
|
||||
- 线性计算
|
||||
|
||||
$$
|
||||
z2_1 = a1_{1} w2_{11} + a1_{2} w2_{21} + b2_{1}
|
||||
$$
|
||||
$$
|
||||
Z2 = A1 \cdot W2 + B2
|
||||
$$
|
||||
|
||||
- 分类函数
|
||||
|
||||
$$a2_1 = Logistic(z2_1)$$
|
||||
$$A2 = Logistic(Z2)$$
|
||||
|
||||
#### 损失函数
|
||||
|
||||
我们把异或问题归类成二分类问题,所以使用二分类交叉熵损失函数:
|
||||
|
||||
$$
|
||||
loss = -Y \ln A2 + (1-Y) \ln (1-A2) \tag{12}
|
||||
$$
|
||||
|
||||
在二分类问题中,$Y,A2$都是一个单一的数值,而非矩阵,但是为了前后统一,我们可以把它们看作是一个$1\times 1$的矩阵。
|
||||
|
||||
### 10.2.3 反向传播
|
||||
|
||||
图10-9展示了反向传播的过程。
|
||||
|
||||
<img src="./img/10/binary_backward.png" />
|
||||
|
||||
图10-9 反向传播过程
|
||||
|
||||
#### 求损失函数对输出层的反向误差
|
||||
|
||||
对损失函数求导,可以得到损失函数对输出层的梯度值,即图10-9中的$Z2$部分。
|
||||
|
||||
根据公式12,求$A2$和$Z2$的导数(此处$A2,Z2,Y$可以看作是标量,以方便求导):
|
||||
|
||||
$$
|
||||
\begin{aligned}
|
||||
\frac{\partial loss}{\partial Z2}&=\frac{\partial loss}{\partial A2}\frac{\partial A2}{\partial Z2} \\\\
|
||||
&=\frac{A2-Y}{A2(1-A2)} \cdot A2(1-A2) \\\\
|
||||
&=A2-Y \rightarrow dZ2
|
||||
\end{aligned}
|
||||
\tag{13}
|
||||
$$
|
||||
|
||||
#### 求$W2$和$B2$的梯度
|
||||
|
||||
$$
|
||||
\begin{aligned}
|
||||
\frac{\partial loss}{\partial W2}&=\begin{pmatrix}
|
||||
\frac{\partial loss}{\partial w2_{11}} \\\\
|
||||
\frac{\partial loss}{\partial w2_{21}}
|
||||
\end{pmatrix}
|
||||
=\begin{pmatrix}
|
||||
\frac{\partial loss}{\partial Z2}\frac{\partial z2}{\partial w2_{11}} \\\\
|
||||
\frac{\partial loss}{\partial Z2}\frac{\partial z2}{\partial w2_{21}}
|
||||
\end{pmatrix}
|
||||
\\\\
|
||||
&=\begin{pmatrix}
|
||||
dZ2 \cdot a1_{1} \\\\
|
||||
dZ2 \cdot a1_{2}
|
||||
\end{pmatrix}
|
||||
=\begin{pmatrix}
|
||||
a1_{1} \\\\ a1_{2}
|
||||
\end{pmatrix}dZ2
|
||||
\\\\
|
||||
&=A1^{\top} \cdot dZ2 \rightarrow dW2
|
||||
\end{aligned}
|
||||
\tag{14}
|
||||
$$
|
||||
$$\frac{\partial{loss}}{\partial{B2}}=dZ2 \rightarrow dB2 \tag{15}$$
|
||||
|
||||
#### 求损失函数对隐层的反向误差
|
||||
|
||||
$$
|
||||
\begin{aligned}
|
||||
\frac{\partial{loss}}{\partial{A1}} &= \begin{pmatrix}
|
||||
\frac{\partial loss}{\partial a1_{1}} & \frac{\partial loss}{\partial a1_{2}}
|
||||
\end{pmatrix}
|
||||
\\\\
|
||||
&=\begin{pmatrix}
|
||||
\frac{\partial{loss}}{\partial{Z2}} \frac{\partial{Z2}}{\partial{a1_{1}}} & \frac{\partial{loss}}{\partial{Z2}} \frac{\partial{Z2}}{\partial{a1_{2}}}
|
||||
\end{pmatrix}
|
||||
\\\\
|
||||
&=\begin{pmatrix}
|
||||
dZ2 \cdot w2_{11} & dZ2 \cdot w2_{21}
|
||||
\end{pmatrix}
|
||||
\\\\
|
||||
&=dZ2 \cdot \begin{pmatrix}
|
||||
w2_{11} & w2_{21}
|
||||
\end{pmatrix}
|
||||
\\\\
|
||||
&=dZ2 \cdot W2^{\top}
|
||||
\end{aligned}
|
||||
\tag{16}
|
||||
$$
|
||||
|
||||
$$
|
||||
\frac{\partial A1}{\partial Z1}=A1 \odot (1-A1) \rightarrow dA1\tag{17}
|
||||
$$
|
||||
|
||||
所以最后到达$Z1$的误差矩阵是:
|
||||
|
||||
$$
|
||||
\begin{aligned}
|
||||
\frac{\partial loss}{\partial Z1}&=\frac{\partial loss}{\partial A1}\frac{\partial A1}{\partial Z1}
|
||||
\\\\
|
||||
&=dZ2 \cdot W2^{\top} \odot dA1 \rightarrow dZ1
|
||||
\end{aligned}
|
||||
\tag{18}
|
||||
$$
|
||||
|
||||
有了$dZ1$后,再向前求$W1$和$B1$的误差,就和第5章中一样了,我们直接列在下面:
|
||||
|
||||
$$
|
||||
dW1=X^{\top} \cdot dZ1 \tag{19}
|
||||
$$
|
||||
$$
|
||||
dB1=dZ1 \tag{20}
|
||||
$$
|
||||
|
|
|
@ -3,133 +3,12 @@
|
|||
|
||||
## 10.3 实现逻辑异或门
|
||||
|
||||
以下为本小节目录,详情请参阅《智能之门》正版图书,高等教育出版社。
|
||||
|
||||
### 10.3.1 代码实现
|
||||
|
||||
#### 准备数据
|
||||
|
||||
异或数据比较简单,只有4个记录,所以就hardcode在此,不用再建立数据集了。这也给读者一个机会了解如何从`DataReader`类派生出一个全新的子类`XOR_DataReader`。
|
||||
|
||||
比如在下面的代码中,我们覆盖了父类中的三个方法:
|
||||
|
||||
- `init()` 初始化方法:因为父类的初始化方法要求有两个参数,代表train/test数据文件
|
||||
- `ReadData()`方法:父类方法是直接读取数据文件,此处直接在内存中生成样本数据,并且直接令训练集等于原始数据集(不需要归一化),令测试集等于训练集
|
||||
- `GenerateValidationSet()`方法,由于只有4个样本,所以直接令验证集等于训练集
|
||||
|
||||
因为`NeuralNet2`中的代码要求数据集比较全,有训练集、验证集、测试集,为了已有代码能顺利跑通,我们把验证集、测试集都设置成与训练集一致,对于解决这个异或问题没有什么影响。
|
||||
|
||||
```Python
|
||||
class XOR_DataReader(DataReader):
|
||||
def ReadData(self):
|
||||
self.XTrainRaw = np.array([0,0,0,1,1,0,1,1]).reshape(4,2)
|
||||
self.YTrainRaw = np.array([0,1,1,0]).reshape(4,1)
|
||||
self.XTrain = self.XTrainRaw
|
||||
self.YTrain = self.YTrainRaw
|
||||
self.num_category = 1
|
||||
self.num_train = self.XTrainRaw.shape[0]
|
||||
self.num_feature = self.XTrainRaw.shape[1]
|
||||
self.XTestRaw = self.XTrainRaw
|
||||
self.YTestRaw = self.YTrainRaw
|
||||
self.XTest = self.XTestRaw
|
||||
self.YTest = self.YTestRaw
|
||||
self.num_test = self.num_train
|
||||
|
||||
def GenerateValidationSet(self, k = 10):
|
||||
self.XVld = self.XTrain
|
||||
self.YVld = self.YTrain
|
||||
```
|
||||
|
||||
#### 测试函数
|
||||
|
||||
与第6章中的逻辑与门和或门一样,我们需要神经网络的运算结果达到一定的精度,也就是非常的接近0,1两端,而不是说勉强大于0.5就近似为1了,所以精度要求是误差绝对值小于`1e-2`。
|
||||
|
||||
```Python
|
||||
def Test(dataReader, net):
|
||||
print("testing...")
|
||||
X,Y = dataReader.GetTestSet()
|
||||
A = net.inference(X)
|
||||
diff = np.abs(A-Y)
|
||||
result = np.where(diff < 1e-2, True, False)
|
||||
if result.sum() == dataReader.num_test:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
```
|
||||
|
||||
#### 主过程代码
|
||||
|
||||
```Python
|
||||
if __name__ == '__main__':
|
||||
......
|
||||
n_input = dataReader.num_feature
|
||||
n_hidden = 2
|
||||
n_output = 1
|
||||
eta, batch_size, max_epoch = 0.1, 1, 10000
|
||||
eps = 0.005
|
||||
hp = HyperParameters2(n_input, n_hidden, n_output, eta, max_epoch, batch_size, eps, NetType.BinaryClassifier, InitialMethod.Xavier)
|
||||
net = NeuralNet2(hp, "Xor_221")
|
||||
net.train(dataReader, 100, True)
|
||||
......
|
||||
```
|
||||
|
||||
此处的代码有几个需要强调的细节:
|
||||
|
||||
- `n_input = dataReader.num_feature`,值为2,而且必须为2,因为只有两个特征值
|
||||
- `n_hidden=2`,这是人为设置的隐层神经元数量,可以是大于2的任何整数
|
||||
- `eps`精度=0.005是后验知识,笔者通过测试得到的停止条件,用于方便案例讲解
|
||||
- 网络类型是`NetType.BinaryClassifier`,指明是二分类网络
|
||||
- 最后要调用`Test`函数验证精度
|
||||
|
||||
### 10.3.2 运行结果
|
||||
|
||||
经过快速的迭代后,会显示训练过程如图10-10所示。
|
||||
|
||||
<img src="./img/10/xor_loss.png" />
|
||||
|
||||
图10-10 训练过程中的损失函数值和准确率值的变化
|
||||
|
||||
可以看到二者的走势很理想。
|
||||
|
||||
同时在控制台会打印一些信息,最后几行如下:
|
||||
|
||||
```
|
||||
......
|
||||
epoch=5799, total_iteration=23199
|
||||
loss_train=0.005553, accuracy_train=1.000000
|
||||
loss_valid=0.005058, accuracy_valid=1.000000
|
||||
epoch=5899, total_iteration=23599
|
||||
loss_train=0.005438, accuracy_train=1.000000
|
||||
loss_valid=0.004952, accuracy_valid=1.000000
|
||||
W= [[-7.10166559 5.48008579]
|
||||
[-7.10286572 5.48050039]]
|
||||
B= [[ 2.91305831 -8.48569781]]
|
||||
W= [[-12.06031599]
|
||||
[-12.26898815]]
|
||||
B= [[5.97067802]]
|
||||
testing...
|
||||
1.0
|
||||
None
|
||||
testing...
|
||||
A2= [[0.00418973]
|
||||
[0.99457721]
|
||||
[0.99457729]
|
||||
[0.00474491]]
|
||||
True
|
||||
```
|
||||
一共用了5900个`epoch`,达到了指定的`loss`精度(0.005),`loss_valid`是0.004991,刚好小于0.005时停止迭代。
|
||||
|
||||
我们特意打印出了`A2`值,即网络推理结果,如表10-7所示。
|
||||
|
||||
表10-7 异或计算值与神经网络推理值的比较
|
||||
|
||||
|x1|x2|XOR|Inference|diff|
|
||||
|---|---|---|---|---|
|
||||
|0|0|0|0.0041|0.0041|
|
||||
|0|1|1|0.9945|0.0055|
|
||||
|1|0|1|0.9945|0.0055|
|
||||
|1|1|0|0.0047|0.0047|
|
||||
|
||||
表中第四列的推理值与第三列的`XOR`结果非常的接近,继续训练的话还可以得到更高的精度,但是一般没这个必要了。由此我们再一次认识到,神经网络只可以得到无限接近真实值的近似解。
|
||||
|
||||
### 代码位置
|
||||
|
||||
ch10, Level1
|
||||
|
|
|
@ -3,304 +3,18 @@
|
|||
|
||||
## 10.4 逻辑异或门的工作原理
|
||||
|
||||
上一节课的内容从实践上证明了两层神经网络是可以解决异或问题的,下面让我们来理解一下神经网络在这个异或问题的上工作原理,此原理可以扩展到更复杂的问题空间,但是由于高维空间无法可视化,给我们的理解带来了困难。
|
||||
以下为本小节目录,详情请参阅《智能之门》正版图书,高等教育出版社。
|
||||
|
||||
### 10.4.1 可视化分类结果
|
||||
|
||||
为了辅助理解异或分类的过程,我们增加一些可视化函数来帮助理解。
|
||||
|
||||
#### 显示原始数据
|
||||
|
||||
```Python
|
||||
import numpy as np
|
||||
from pathlib import Path
|
||||
import matplotlib.pyplot as plt
|
||||
|
||||
from Level1_XorGateClassifier import *
|
||||
|
||||
def ShowSourceData(dataReader):
|
||||
DrawSamplePoints(dataReader.XTrain[:,0],dataReader.XTrain[:,1],dataReader.YTrain, "XOR Source Data", "x1", "x2")
|
||||
|
||||
def DrawSamplePoints(x1, x2, y, title, xlabel, ylabel, show=True):
|
||||
assert(x1.shape[0] == x2.shape[0])
|
||||
fig = plt.figure(figsize=(6,6))
|
||||
count = x1.shape[0]
|
||||
for i in range(count):
|
||||
if y[i,0] == 0:
|
||||
plt.scatter(x1[i], x2[i], marker='^', color='r', s=200, zorder=10)
|
||||
else:
|
||||
plt.scatter(x1[i], x2[i], marker='o', color='b', s=200, zorder=10)
|
||||
#end if
|
||||
#end for
|
||||
plt.grid()
|
||||
plt.title(title)
|
||||
plt.xlabel(xlabel)
|
||||
plt.ylabel(ylabel)
|
||||
if show:
|
||||
plt.show()
|
||||
```
|
||||
|
||||
1. 首先是从Level_XorGateClassifier中导入所有内容,省去了我们重新写数据准备部分的代码的麻烦
|
||||
2. 获得所有分类为1的训练样本,用红色叉子显示在画板上
|
||||
3. 获得所有分类为0的训练样本,用蓝色圆点显示在画板上
|
||||
|
||||
由此我们会得到样本如图10-11所示。
|
||||
|
||||
<img src="./img/10/xor_source_data.png" ch="500" />
|
||||
|
||||
图10-11 异或样本数据
|
||||
|
||||
异或问题的四个点分布在[0,1]空间的四个角上,红色点是正类,蓝色点是负类。
|
||||
|
||||
#### 显示推理的中间结果
|
||||
|
||||
由于是双层神经网络,回忆一下其公式:$Z1 = X \cdot W1 +B1,A1=Sigmoid(Z1),Z2=A1 \cdot W2+B2,A2=Logistic(A2)$,所以会有$Z1,A1,Z2,A2$等中间运算结果。我们把它们用图形方式显示出来帮助读者理解推理过程。
|
||||
|
||||
```Python
|
||||
def ShowProcess2D(net, dataReader):
|
||||
net.inference(dataReader.XTest)
|
||||
# show z1
|
||||
DrawSamplePoints(net.Z1[:,0], net.Z1[:,1], dataReader.YTest, "net.Z1", "Z1[0]", "Z1[1]")
|
||||
# show a1
|
||||
DrawSamplePoints(net.A1[:,0], net.A1[:,1], dataReader.YTest, "net.A1", "A1[0]", "A1[1]")
|
||||
# show sigmoid
|
||||
DrawSamplePoints(net.Z2, net.A2, dataReader.YTrain, "Z2->A2", "Z2", "A2", show=False)
|
||||
x = np.linspace(-6,6)
|
||||
a = Sigmoid().forward(x)
|
||||
plt.plot(x,a)
|
||||
plt.show()
|
||||
```
|
||||
|
||||
1. 先用测试样本做一次推理;
|
||||
2. Z1是第一层神经网络线性变换的结果,由于Z1是一个4行两列的数组,我们以Z1的第1列作为x1,以Z1的第2列作为x2,画出4个点来;
|
||||
3. A1是Z1经过激活函数后的结果,同Z1一样作为4个点画出来;
|
||||
4. Z2是第二层神经网络线性变换的结果,A2是Z2的Logistic Function的运算结果,以Z2为x1,A2为x2,画出4个点来,并叠加Logistic函数图像,看是否吻合。
|
||||
|
||||
于是我们得到下面三张图,放入表10-8中(把原始图作为对比,放在第一个位置)。
|
||||
|
||||
表10-8 XOR问题的推理过程
|
||||
|
||||
|||
|
||||
|---|---|
|
||||
|<img src='./img/10/xor_source_data.png'/>|<img src='./img/10/xor_z1.png'/>|
|
||||
|原始样本|Z1是第一层网络线性计算结果|
|
||||
|<img src='./img/10/xor_a1.png'/>|<img src='./img/10/xor_z2_a2.png'/>|
|
||||
|A1是Z1的激活函数计算结果|Z2是第二层线性计算结果,A2是二分类结果|
|
||||
|
||||
- Z1:通过线性变换,把原始数据蓝色点移动到两个对角上,把红色点向中心移动,接近重合。图中的红色点看上去好像是一个点,实际上是两个点重合在了一起,可以通过在原画板上放大的方式来看细节
|
||||
- A1:通过Sigmoid运算,把Z1的值压缩到了[0,1]空间内,使得蓝色点的坐标向[0,1]和[1,0]接近,红色点的坐标向[0,0]靠近
|
||||
- Z2->A2:再次通过线性变换,把两类点都映射到横坐标轴上,并把蓝点向负方向移动,把红点向正方向移动,再Logistic分类,把两类样本点远远地分开到[0,1]的两端,从而完成分类任务
|
||||
|
||||
我们把中间计算结果显示在表10-9中,便于观察比较。
|
||||
|
||||
表10-9 中间计算结果
|
||||
|
||||
||1(蓝点1)|2(红点1)|3(红点2)|4(蓝点2)|
|
||||
|---|---|---|---|---|
|
||||
|x1|0|0|1|1|
|
||||
|x2|0|1|0|1|
|
||||
|y|0|1|1|0|
|
||||
|Z1|2.868856|-4.142354|-4.138914|-11.150125|
|
||||
||-8.538638|-3.024127|-3.023451|2.491059|
|
||||
|A1|0.946285|0.015637|0.015690|0.000014|
|
||||
||0.000195|0.046347|0.046377|0.923512|
|
||||
|Z2|-5.458510|5.203479|5.202473|-5.341711|
|
||||
|A2|0.004241|0.994532|0.994527|0.004764|
|
||||
|
||||
#### 显示最后结果
|
||||
|
||||
到目前位置,我们只知道神经网络完成了异或问题,但它究竟是如何画分割线的呢?
|
||||
|
||||
也许读者还记得在第四步中学习线性分类的时候,我们成功地通过公式推导画出了分割直线,但是这一次不同了,这里使用了两层神经网络,很难再通过公式推导来解释W和B权重矩阵的含义了,所以我们换个思路。
|
||||
|
||||
思考一下,神经网络最后是通过什么方式判定样本的类别呢?在前向计算过程中,最后一个公式是Logistic函数,它把$(-\infty, +\infty)$压缩到了(0,1)之间,相当于计算了一个概率值,然后通过概率值大于0.5与否,判断是否属于正类。虽然异或问题只有4个样本点,但是如果:
|
||||
|
||||
1. 我们在[0,1]正方形区间内进行网格状均匀采样,这样每个点都会有坐标值;
|
||||
2. 再把坐标值代入神经网络进行推理,得出来的应该会是一个网格状的结果;
|
||||
3. 每个结果都是一个概率值,肯定处于(0,1)之间,所以不是大于0.5,就是小于0.5;
|
||||
4. 我们把大于0.5的网格涂成粉色,把小于0.5的网格涂成黄色,就应该可以画出分界线来了。
|
||||
|
||||
好,有了这个令人激动人心的想法,我们立刻实现:
|
||||
|
||||
```Python
|
||||
def ShowResult2D(net, dr, title):
|
||||
print("please wait for a while...")
|
||||
DrawSamplePoints(dr.XTest[:,0], dr.XTest[:,1], dr.YTest, title, "x1", "x2", show=False)
|
||||
count = 50
|
||||
x1 = np.linspace(0,1,count)
|
||||
x2 = np.linspace(0,1,count)
|
||||
for i in range(count):
|
||||
for j in range(count):
|
||||
x = np.array([x1[i],x2[j]]).reshape(1,2)
|
||||
output = net.inference(x)
|
||||
if output[0,0] >= 0.5:
|
||||
plt.plot(x[0,0], x[0,1], 's', c='m', zorder=1)
|
||||
else:
|
||||
plt.plot(x[0,0], x[0,1], 's', c='y', zorder=1)
|
||||
# end if
|
||||
# end for
|
||||
# end for
|
||||
plt.title(title)
|
||||
plt.show()
|
||||
```
|
||||
|
||||
在上面的代码中,横向和竖向各取了50个点,形成一个50x50的网格,然后依次推理,得到output值后染色。由于一共要计算2500次,所以花费的时间稍长,我们打印"please wait for a while..."让程序跑一会儿。最后得到图10-12。
|
||||
|
||||
<img src="./img/10/xor_result_2d.png" ch="500" />
|
||||
|
||||
图10-12 分类结果的分割图
|
||||
|
||||
第一次看到这张图是不是很激动!从此我们不再靠画线过日子了,而是上升到了染色的层次!请忽略图中的锯齿,因为我们取了50x50的网格,所以会有马赛克,如果取更密集的网格点,会缓解这个问题,但是计算速度要慢很多倍。
|
||||
|
||||
可以看到,两类样本点被分在了不同颜色的区域内,这让我们恍然大悟,原来神经网络可以同时画两条分割线的,更准确的说法是“可以画出两个分类区域”。
|
||||
|
||||
### 10.4.2 更直观的可视化结果
|
||||
|
||||
#### 3D图
|
||||
|
||||
神经网络真的可以同时画两条分割线吗?这颠覆了笔者的认知,因为笔者一直认为最后一层的神经网络只是一个线性单元,它能做的事情有限,所以它的行为就是线性的行为,画一条线做拟合或分割,......,稍等,为什么只能是一条线呢?难道不可以是一个平面吗?
|
||||
|
||||
这让笔者想起了在第5章里,曾经用一个平面拟合了空间中的样本点,如表10-10所示。
|
||||
|
||||
表10-10 平面拟合的可视化结果
|
||||
|
||||
|正向|侧向|
|
||||
|---|---|
|
||||
|<img src='./img/5/level3_result_1.png'/>|<img src='./img/5/level3_result_2.png'/>|
|
||||
|
||||
那么这个异或问题的解是否可能是个立体空间呢?有了这个更激动人心的想法,我们立刻写代码:
|
||||
|
||||
```Python
|
||||
def Prepare3DData(net, count):
|
||||
x = np.linspace(0,1,count)
|
||||
y = np.linspace(0,1,count)
|
||||
X, Y = np.meshgrid(x, y)
|
||||
Z = np.zeros((count, count))
|
||||
input = np.hstack((X.ravel().reshape(count*count,1),Y.ravel().reshape(count*count,1)))
|
||||
output = net.inference(input)
|
||||
Z = output.reshape(count,count)
|
||||
return X,Y,Z
|
||||
|
||||
def ShowResult3D(net, dr):
|
||||
fig = plt.figure(figsize=(6,6))
|
||||
ax = Axes3D(fig)
|
||||
X,Y,Z = Prepare3DData(net, 50)
|
||||
ax.plot_surface(X,Y,Z,cmap='rainbow')
|
||||
ax.set_zlim(0,1)
|
||||
# draw sample data in 3D space
|
||||
for i in range(dr.num_train):
|
||||
if dataReader.YTrain[i,0] == 0:
|
||||
ax.scatter(dataReader.XTrain[i,0],dataReader.XTrain[i,1],dataReader.YTrain[i,0],marker='^',c='r',s=200)
|
||||
else:
|
||||
ax.scatter(dataReader.XTrain[i,0],dataReader.XTrain[i,1],dataReader.YTrain[i,0],marker='o',c='b',s=200)
|
||||
|
||||
plt.show()
|
||||
```
|
||||
|
||||
函数Prepare3DData()用于准备一个三维坐标系内的数据:
|
||||
|
||||
1. x坐标在[0,1]空间分成50份
|
||||
2. y坐标在[0,1]空间分成50份
|
||||
3. np.meshgrid(x,y)形成网格式点阵X和Y,它们各有2500个记录,每一行的X必须和对应行的Y组合使用形成网点
|
||||
4. np.hstack()把X,Y合并成2500x2的样本矩阵
|
||||
5. net.inference()做推理,得到结果output
|
||||
6. 把结果再转成50x50的形状并赋值给Z,与X、Y的50x50的网格点匹配
|
||||
7. 最后返回三维点阵XYZ
|
||||
8. 函数ShowResult3D()使用ax.plot_surface()函数绘制空间曲面
|
||||
9. 然后在空间中绘制4个样本点,X和Y值就是原始的样本值x1和x2,Z值是原始的标签值y,即0或1
|
||||
|
||||
最后得到表10-11的结果。
|
||||
|
||||
表10-11 异或分类结果可视化
|
||||
|
||||
|斜侧视角|顶视角|
|
||||
|---|---|
|
||||
|<img src='./img/10/xor_result_3D_1.png'/>|<img src='./img/10/xor_result_3D_2.png'/>|
|
||||
|
||||
这下子我们立刻就明白了神经网络都做了些什么事情:它通过样本点,推算出了平面上每个坐标点的分类结果概率,形成空间曲面,然后拦腰一刀(一个切面),这样神经网络就可以在Z=0.5出画一个平面,完美地分开对角顶点。如果看顶视图,与我们在前面生成的2D区域染色图极为相似,它的红色区域的概率值接近于1,蓝色区域的概率值接近于0,在红蓝之间的颜色,代表了从0到1的渐变值。
|
||||
|
||||
平面上分割两类的直线,只是我们的想象:使用0.5为门限值像国界一样把两部分数据分开。但实际上,神经网络的输出是个概率,即,它可以告诉你某个点属于某个类别的概率是多少,我们人为地设定为当概率大于0.5时属于正类,小于0.5时属于负类。在空间曲面中,可以把过渡区也展示出来,让大家更好地理解。
|
||||
|
||||
#### 2.5D图
|
||||
|
||||
3D图虽然有趣,但是2D图已经能表达分类的意思了,只是不完美,那我们想办法做一个2.5D图吧。
|
||||
|
||||
```Python
|
||||
def ShowResultContour(net, dr):
|
||||
DrawSamplePoints(dr.XTrain[:,0], dr.XTrain[:,1], dr.YTrain, "classification result", "x1", "x2", show=False)
|
||||
X,Y,Z = Prepare3DData(net, 50)
|
||||
plt.contourf(X, Y, Z, cmap=plt.cm.Spectral)
|
||||
plt.show()
|
||||
```
|
||||
|
||||
在二维平面上,可以通过plt.contourf()函数画出着色的等高线图,Z作为等高线高度,可以得到图10-13。
|
||||
|
||||
<img src="./img/10/xor_result_25d.png" ch="500" />
|
||||
|
||||
图10-13 分类结果的等高线图
|
||||
|
||||
2.5D图通过颜色来表示不同区域的概率值,可以看到红色区和蓝色区分别是概率接近于0和1的区域,对应着两类样本点。我们后面将会使用这种方式继续研究分类问题。
|
||||
|
||||
但是神经网络真的可以聪明到用升维的方式来解决问题吗?我们只是找到了一种能自圆其说的解释,但是不能确定神经网络就是这样工作的。下面我们会通过探查神经网络的训练过程,来理解它究竟是怎样学习的。
|
||||
|
||||
### 10.4.3 探查训练的过程
|
||||
|
||||
随着迭代次数的增加,对异或二分类问题的分类结果也越来越精确,我们不妨观察一下训练过程中的几个阶段,来试图理解神经网络的训练过程。
|
||||
|
||||
在下面的试验中,我们指定500、2000、6000三个迭代次数,来查看各个阶段的分类情况。
|
||||
|
||||
表10-12 异或分类训练过程中Z1和A1的值的演变
|
||||
|
||||
|迭代次数|Z1的演变|A1的演变|
|
||||
|---|---|---|
|
||||
|500次|<img src='./img/10/xor_z1_500.png'/>|<img src='./img/10/xor_a1_500.png'/>|
|
||||
|2000次|<img src='./img/10/xor_z1_2000.png'/>|<img src='./img/10/xor_a1_2000.png'/>|
|
||||
|6000次|<img src='./img/10/xor_z1_6000.png'/>|<img src='./img/10/xor_a1_6000.png'/>|
|
||||
|
||||
从上图Z1演变过程看,神经网络试图使得两个红色的点重合,而两个蓝色的点距离越远越好,但是中心对称的。
|
||||
|
||||
从A1的演变过程看,和Z1差不多,但最后的目的是使得红色点处于[0,1]空间两个顶点(和原始数据一样的位置),蓝色点重合于一个角落。从A1的演变过程最后一张图来看,两个红色点已经被挤压到了一起,所以完全可以有一根分割线把二者分开,如图10-14所示。
|
||||
|
||||
<img src="./img/10/xor_a1_6000_line.png" ch="500" />
|
||||
|
||||
图10-14 经过空间变换后的两类样本数据
|
||||
|
||||
也就是说到了这一步,神经网络已经不需要做升维演算了,在二维平面上就可以解决分类问题了。从笔者个人的观点出发,更愿意相信这才是神经网络的工作原理。
|
||||
|
||||
下面我们再看看最后的分类结果的演变过程,如表10-13所示。
|
||||
|
||||
表10-13 异或分类训练过程中分类函数值和结果的演变
|
||||
|
||||
|迭代次数|分类函数值的演变|分类结果的演变|
|
||||
|---|---|---|
|
||||
|500次|<img src='./img/10/xor_logistic_500.png'/>|<img src='./img/10/xor_result_500.png'/>|
|
||||
|2000次|<img src='./img/10/xor_logistic_2000.png'/>|<img src='./img/10/xor_result_2000.png'/>|
|
||||
|6000次|<img src='./img/10/xor_logistic_6000.png'/>|<img src='./img/10/xor_result_6000.png'/>|
|
||||
|
||||
从分类函数情况看,开始时完全分不开两类点,随着学习过程的加深,两类点逐步地向两端移动,直到最后尽可能地相距很远。从分类结果的2.5D图上,可以看出这个方形区域内的每个点的概率变化,由于样本点的对称分布,最后形成了带状的概率分布图。
|
||||
|
||||
### 10.4.4 隐层神经元数量的影响
|
||||
|
||||
一般来说,隐层的神经元数量要大于等于输入特征的数量,在本例中特征值数量是2。出于研究目的,笔者使用了6种数量的神经元配置来试验神经网络的工作情况,请看表10-14中的比较图。
|
||||
|
||||
表10-14 隐层神经元数量对分类结果的影响
|
||||
|
||||
|||
|
||||
|---|---|
|
||||
|<img src='./img/10/xor_n1.png'/>|<img src='./img/10/xor_n2.png'/>|
|
||||
|1个神经元,无法完成分类任务|2个神经元,迭代6200次到达精度要求|
|
||||
|<img src='./img/10/xor_n3.png'/>|<img src='./img/10/xor_n4.png'/>|
|
||||
|3个神经元,迭代4900次到达精度要求|4个神经元,迭代4300次到达精度要求|
|
||||
|<img src='./img/10/xor_n8.png'/>|<img src='./img/10/xor_n16.png'/>|
|
||||
|8个神经元,迭代4400次到达精度要求|16个神经元,迭代4500次到达精度要求|
|
||||
|
||||
以上各情况的迭代次数是在Xavier初始化的情况下测试一次得到的数值,并不意味着神经元越多越好,合适的数量才好。总结如下:
|
||||
|
||||
- 2个神经元肯定是足够的;
|
||||
- 4个神经元肯定要轻松一些,用的迭代次数最少。
|
||||
- 而更多的神经元也并不是更轻松,比如8个神经元,杀鸡用了宰牛刀,由于功能过于强大,出现了曲线的分类边界;
|
||||
- 而16个神经元更是事倍功半地把4个样本分到了4个区域上,当然这也给了我们一些暗示:神经网络可以做更强大的事情!
|
||||
- 表中图3的分隔带角度与前面几张图相反,但是红色样本点仍处于蓝色区,蓝色样本点仍处于红色区,这个性质没有变。这只是初始化参数不同造成的神经网络的多个解,与神经元数量无关。
|
||||
|
||||
### 代码位置
|
||||
|
||||
|
|
|
@ -3,62 +3,12 @@
|
|||
|
||||
## 10.5 实现双弧形二分类
|
||||
|
||||
逻辑异或问题的成功解决,可以带给我们一定的信心,但是毕竟只有4个样本,还不能发挥出双层神经网络的真正能力。下面让我们一起来解决问题二,复杂的二分类问题。
|
||||
以下为本小节目录,详情请参阅《智能之门》正版图书,高等教育出版社。
|
||||
|
||||
### 10.5.1 代码实现
|
||||
|
||||
#### 主过程代码
|
||||
|
||||
```Python
|
||||
if __name__ == '__main__':
|
||||
......
|
||||
n_input = dataReader.num_feature
|
||||
n_hidden = 2
|
||||
n_output = 1
|
||||
eta, batch_size, max_epoch = 0.1, 5, 10000
|
||||
eps = 0.08
|
||||
|
||||
hp = HyperParameters2(n_input, n_hidden, n_output, eta, max_epoch, batch_size, eps, NetType.BinaryClassifier, InitialMethod.Xavier)
|
||||
net = NeuralNet2(hp, "Arc_221")
|
||||
net.train(dataReader, 5, True)
|
||||
net.ShowTrainingTrace()
|
||||
```
|
||||
|
||||
此处的代码有几个需要强调的细节:
|
||||
|
||||
- `n_input = dataReader.num_feature`,值为2,而且必须为2,因为只有两个特征值
|
||||
- `n_hidden=2`,这是人为设置的隐层神经元数量,可以是大于2的任何整数
|
||||
- `eps`精度=0.08是后验知识,笔者通过测试得到的停止条件,用于方便案例讲解
|
||||
- 网络类型是`NetType.BinaryClassifier`,指明是二分类网络
|
||||
|
||||
### 10.5.2 运行结果
|
||||
|
||||
经过快速的迭代,训练完毕后,会显示损失函数曲线和准确率曲线如图10-15。
|
||||
|
||||
<img src="./img/10/sin_loss.png" />
|
||||
|
||||
图10-15 训练过程中的损失函数值和准确率值的变化
|
||||
|
||||
蓝色的线条是小批量训练样本的曲线,波动相对较大,不必理会,因为批量小势必会造成波动。红色曲线是验证集的走势,可以看到二者的走势很理想,经过一小段时间的磨合后,从第200个`epoch`开始,两条曲线都突然找到了突破的方向,然后只用了50个`epoch`,就迅速达到指定精度。
|
||||
|
||||
同时在控制台会打印一些信息,最后几行如下:
|
||||
|
||||
```
|
||||
......
|
||||
epoch=259, total_iteration=18719
|
||||
loss_train=0.092687, accuracy_train=1.000000
|
||||
loss_valid=0.074073, accuracy_valid=1.000000
|
||||
W= [[ 8.88189429 6.09089509]
|
||||
[-7.45706681 5.07004428]]
|
||||
B= [[ 1.99109895 -7.46281087]]
|
||||
W= [[-9.98653838]
|
||||
[11.04185384]]
|
||||
B= [[3.92199463]]
|
||||
testing...
|
||||
1.0
|
||||
```
|
||||
一共用了260个`epoch`,达到了指定的loss精度(0.08)时停止迭代。看测试集的情况,准确度1.0,即100%分类正确。
|
||||
|
||||
### 代码位置
|
||||
|
||||
ch10, Level3
|
||||
|
|
|
@ -3,127 +3,13 @@
|
|||
|
||||
## 10.6 双弧形二分类的工作原理
|
||||
|
||||
在异或问题中,我们知道了如果使用三维坐标系来分析平面上任意复杂的分类问题,都可以迎刃而解:只要把不同的类别的点通过三维线性变换把它们向上升起,就很容易地分开不同类别的样本。但是这种解释有些牵强,笔者不认为神经网络已经聪明到这个程度了。
|
||||
|
||||
所以,笔者试图在二维平面上继续研究,寻找真正的答案,恰巧读到了关于流式学习的一些资料,于是做了下述试验,来验证神经网络到底在二维平面上做了什么样的空间变换。
|
||||
以下为本小节目录,详情请参阅《智能之门》正版图书,高等教育出版社。
|
||||
|
||||
### 10.6.1 两层神经网络的可视化
|
||||
|
||||
#### 几个辅助的函数
|
||||
- `DrawSamplePoints(x1, x2, y, title, xlabel, ylabel, show=True)`
|
||||
|
||||
画样本点,把正例绘制成红色的`x`,把负例绘制成蓝色的点。输入的`x1`和`x2`组成横纵坐标,`y`是正负例的标签值。
|
||||
|
||||
- `Prepare3DData(net, count)
|
||||
|
||||
准备3D数据,把平面划分成`count` * `count`的网格,并形成矩阵。如果传入的`net`不是None的话,会使用`net.inference()`做一次推理,以便得到和平面上的网格相对应的输出值。
|
||||
|
||||
- `DrawGrid(Z, count)`
|
||||
|
||||
绘制网格。这个网格不一定是正方形的,有可能会由于矩阵的平移缩放而扭曲,目的是观察神经网络对空间的变换。
|
||||
|
||||
- `ShowSourceData(dataReader)`
|
||||
|
||||
显示原始训练样本数据。
|
||||
|
||||
- `ShowTransformation(net, dr, epoch)`
|
||||
|
||||
绘制经过神经网络第一层的线性计算即激活函数计算后,空间变换的结果。神经网络的第二层就是在第一层的空间变换的结果之上来完成分类任务的。
|
||||
|
||||
- `ShowResult2D(net, dr, epoch)`
|
||||
|
||||
在二维平面上显示分类结果,实际是用等高线方式显示2.5D的分类结果。
|
||||
|
||||
#### 训练函数
|
||||
|
||||
```Python
|
||||
def train(dataReader, max_epoch):
|
||||
n_input = dataReader.num_feature
|
||||
n_hidden = 2
|
||||
n_output = 1
|
||||
eta, batch_size = 0.1, 5
|
||||
eps = 0.01
|
||||
|
||||
hp = HyperParameters2(n_input, n_hidden, n_output, eta, max_epoch, batch_size, eps, NetType.BinaryClassifier, InitialMethod.Xavier)
|
||||
net = NeuralNet2(hp, "Arc_221_epoch")
|
||||
|
||||
net.train(dataReader, 5, True)
|
||||
|
||||
ShowTransformation(net, dataReader, max_epoch)
|
||||
ShowResult2D(net, dataReader, max_epoch)
|
||||
```
|
||||
接收`max_epoch`做为参数,控制神经网络训练迭代的次数,以此来观察中间结果。我们使用了如下超参:
|
||||
|
||||
- `n_input=2`,输入的特征值数量
|
||||
- `n_hidden=2`,隐层的神经元数
|
||||
- `n_output=1`,输出为二分类
|
||||
- `eta=0.1`,学习率
|
||||
- `batch_size=5`,批量样本数为5
|
||||
- `eps=0.01`,停止条件
|
||||
- `NetType.BinaryClassifier`,二分类网络
|
||||
- `InitialMethod.Xavier`,初始化方法为Xavier
|
||||
|
||||
每迭代5次做一次损失值计算,打印一次结果。最后显示中间状态图和分类结果图。
|
||||
|
||||
#### 主过程
|
||||
|
||||
```Python
|
||||
if __name__ == '__main__':
|
||||
dataReader = DataReader(train_data_name, test_data_name)
|
||||
dataReader.ReadData()
|
||||
dataReader.NormalizeX()
|
||||
dataReader.Shuffle()
|
||||
dataReader.GenerateValidationSet()
|
||||
|
||||
ShowSourceData(dataReader)
|
||||
plt.show()
|
||||
|
||||
train(dataReader, 20)
|
||||
train(dataReader, 50)
|
||||
train(dataReader, 100)
|
||||
train(dataReader, 150)
|
||||
train(dataReader, 200)
|
||||
train(dataReader, 600)
|
||||
```
|
||||
读取数据后,以此用20、50、100、150、200、600个`epoch`来做为训练停止条件,以便观察中间状态,笔者经过试验事先知道了600次迭代一定可以达到满意的效果。而上述`epoch`的取值,是通过观察损失函数的下降曲线来确定的。
|
||||
|
||||
### 10.6.2 运行结果
|
||||
|
||||
运行后,首先会显示一张原始样本的位置如图10-16,以便确定训练样本是否正确,并得到基本的样本分布概念。
|
||||
|
||||
<img src="./img/10/sin_data_source.png" ch="500" />
|
||||
|
||||
图10-16 双弧形的样本数据
|
||||
|
||||
随着每一个`train()`函数的调用,会在每一次训练结束后依次显示以下图片:
|
||||
|
||||
- 第一层神经网络的线性变换结果
|
||||
- 第一层神经网络的激活函数结果
|
||||
- 第二层神经网络的分类结果
|
||||
|
||||
表10-15 训练过程可视化
|
||||
|
||||
|迭代|线性变换|激活结果|分类结果|
|
||||
|---|---|---|---|
|
||||
|20次|<img src='./img/10/sin_z1_20.png'/>|<img src='./img/10/sin_a1_20.png'/>|<img src='./img/10/sin_a2_20.png'/>|
|
||||
|100次|<img src='./img/10/sin_z1_100.png'/>|<img src='./img/10/sin_a1_100.png'/>|<img src='./img/10/sin_a2_100.png'/>|
|
||||
|200次|<img src='./img/10/sin_z1_200.png'/>|<img src='./img/10/sin_a1_200.png'/>|<img src='./img/10/sin_a2_200.png'/>|
|
||||
|600次|<img src='./img/10/sin_z1_600.png'/>|<img src='./img/10/sin_a1_600.png'/>|<img src='./img/10/sin_a2_600.png'/>|
|
||||
|
||||
分析表10-15中各列图片的变化,我们可以得到以下结论:
|
||||
|
||||
1. 在第一层的线性变换中,原始样本被斜侧拉伸,角度渐渐左倾到40度,并且样本间距也逐渐拉大,原始样本归一化后在[0,1]之间,最后已经拉到了[-5,15]的范围。这种侧向拉伸实际上是为激活函数做准备。
|
||||
2. 在激活函数计算中,由于激活函数的非线性,所以空间逐渐扭曲变形,使得红色样本点逐步向右下角移动,并变得稠密;而蓝色样本点逐步向左上方扩撒,相信它的极限一定是[0,1]空间的左边界和上边界;另外一个值得重点说明的就是,通过空间扭曲,红蓝两类之间可以用一条直线分割了!这是一件非常神奇的事情。
|
||||
3. 最后的分类结果,从毫无头绪到慢慢向上拱起,然后是宽而模糊的分类边界,最后形成非常锋利的边界。
|
||||
|
||||
似乎到了这里,我们可以得出结论了:神经网络通过空间变换的方式,把线性不可分的样本变成了线性可分的样本,从而给最后的分类变得很容易。
|
||||
|
||||
<img src="./img/10/sin_a1_line.png" ch="500" />
|
||||
|
||||
图10-17 经过空间变换后的样本数据
|
||||
|
||||
如图10-17中的那条绿色直线,很轻松就可以完成二分类任务。这条直线如果还原到原始样本图片中,将会是上表中第四列的分类结果的最后一张图的样子。
|
||||
|
||||
### 思考与练习
|
||||
|
||||
1. 请使用同样的方法分析异或问题。
|
||||
|
|
|
@ -3,242 +3,17 @@
|
|||
|
||||
## 11.1 非线性多分类
|
||||
|
||||
以下为本小节目录,详情请参阅《智能之门》正版图书,高等教育出版社。
|
||||
|
||||
### 11.1.1 定义神经网络结构
|
||||
|
||||
先设计出能完成非线性多分类的网络结构,如图11-2所示。
|
||||
|
||||
<img src="./img/11/nn.png" />
|
||||
|
||||
图11-2 非线性多分类的神经网络结构图
|
||||
|
||||
- 输入层两个特征值$x_1, x_2$
|
||||
$$
|
||||
x=
|
||||
\begin{pmatrix}
|
||||
x_1 & x_2
|
||||
\end{pmatrix}
|
||||
$$
|
||||
- 隐层$2\times 3$的权重矩阵$W1$
|
||||
$$
|
||||
W1=
|
||||
\begin{pmatrix}
|
||||
w1_{11} & w1_{12} & w1_{13} \\\\
|
||||
w1_{21} & w1_{22} & w1_{23}
|
||||
\end{pmatrix}
|
||||
$$
|
||||
|
||||
- 隐层$1\times 3$的偏移矩阵$B1$
|
||||
|
||||
$$
|
||||
B1=\begin{pmatrix}
|
||||
b1_1 & b1_2 & b1_3
|
||||
\end{pmatrix}
|
||||
$$
|
||||
|
||||
- 隐层由3个神经元构成
|
||||
- 输出层$3\times 3$的权重矩阵$W2$
|
||||
$$
|
||||
W2=\begin{pmatrix}
|
||||
w2_{11} & w2_{12} & w2_{13} \\\\
|
||||
w2_{21} & w2_{22} & w2_{23} \\\\
|
||||
w2_{31} & w2_{32} & w2_{33}
|
||||
\end{pmatrix}
|
||||
$$
|
||||
|
||||
- 输出层$1\times 1$的偏移矩阵$B2$
|
||||
|
||||
$$
|
||||
B2=\begin{pmatrix}
|
||||
b2_1 & b2_2 & b2_3
|
||||
\end{pmatrix}
|
||||
$$
|
||||
|
||||
- 输出层有3个神经元使用Softmax函数进行分类
|
||||
|
||||
### 11.1.2 前向计算
|
||||
|
||||
根据网络结构,可以绘制前向计算图,如图11-3所示。
|
||||
|
||||
<img src="./img/11/multiple_forward.png" />
|
||||
|
||||
图11-3 前向计算图
|
||||
|
||||
#### 第一层
|
||||
|
||||
- 线性计算
|
||||
|
||||
$$
|
||||
z1_1 = x_1 w1_{11} + x_2 w1_{21} + b1_1
|
||||
$$
|
||||
$$
|
||||
z1_2 = x_1 w1_{12} + x_2 w1_{22} + b1_2
|
||||
$$
|
||||
$$
|
||||
z1_3 = x_1 w1_{13} + x_2 w1_{23} + b1_3
|
||||
$$
|
||||
$$
|
||||
Z1 = X \cdot W1 + B1
|
||||
$$
|
||||
|
||||
- 激活函数
|
||||
|
||||
$$
|
||||
a1_1 = Sigmoid(z1_1)
|
||||
$$
|
||||
$$
|
||||
a1_2 = Sigmoid(z1_2)
|
||||
$$
|
||||
$$
|
||||
a1_3 = Sigmoid(z1_3)
|
||||
$$
|
||||
$$
|
||||
A1 = Sigmoid(Z1)
|
||||
$$
|
||||
|
||||
#### 第二层
|
||||
|
||||
- 线性计算
|
||||
|
||||
$$
|
||||
z2_1 = a1_1 w2_{11} + a1_2 w2_{21} + a1_3 w2_{31} + b2_1
|
||||
$$
|
||||
$$
|
||||
z2_2 = a1_1 w2_{12} + a1_2 w2_{22} + a1_3 w2_{32} + b2_2
|
||||
$$
|
||||
$$
|
||||
z2_3 = a1_1 w2_{13} + a1_2 w2_{23} + a1_3 w2_{33} + b2_3
|
||||
$$
|
||||
$$
|
||||
Z2 = A1 \cdot W2 + B2
|
||||
$$
|
||||
|
||||
- 分类函数
|
||||
|
||||
$$
|
||||
a2_1 = \frac{e^{z2_1}}{e^{z2_1} + e^{z2_2} + e^{z2_3}}
|
||||
$$
|
||||
$$
|
||||
a2_2 = \frac{e^{z2_2}}{e^{z2_1} + e^{z2_2} + e^{z2_3}}
|
||||
$$
|
||||
$$
|
||||
a2_3 = \frac{e^{z2_3}}{e^{z2_1} + e^{z2_2} + e^{z2_3}}
|
||||
$$
|
||||
$$
|
||||
A2 = Softmax(Z2)
|
||||
$$
|
||||
|
||||
#### 损失函数
|
||||
|
||||
使用多分类交叉熵损失函数:
|
||||
$$
|
||||
loss = -(y_1 \ln a2_1 + y_2 \ln a2_2 + y_3 \ln a2_3)
|
||||
$$
|
||||
$$
|
||||
J(w,b) = -\frac{1}{m} \sum^m_{i=1} \sum^n_{j=1} y_{ij} \ln (a2_{ij})
|
||||
$$
|
||||
|
||||
$m$为样本数,$n$为类别数。
|
||||
|
||||
### 11.1.3 反向传播
|
||||
|
||||
根据前向计算图,可以绘制出反向传播的路径如图11-4。
|
||||
|
||||
<img src="./img/11/multiple_backward.png" />
|
||||
|
||||
图11-4 反向传播图
|
||||
|
||||
在第7.1中学习过了Softmax与多分类交叉熵配合时的反向传播推导过程,最后是一个很简单的减法:
|
||||
|
||||
$$
|
||||
\frac{\partial loss}{\partial Z2}=A2-y \rightarrow dZ2
|
||||
$$
|
||||
|
||||
从Z2开始再向前推的话,和10.2节是一模一样的,所以直接把结论拿过来:
|
||||
|
||||
$$
|
||||
\frac{\partial loss}{\partial W2}=A1^{\top} \cdot dZ2 \rightarrow dW2
|
||||
$$
|
||||
$$\frac{\partial{loss}}{\partial{B2}}=dZ2 \rightarrow dB2$$
|
||||
$$
|
||||
\frac{\partial A1}{\partial Z1}=A1 \odot (1-A1) \rightarrow dA1
|
||||
$$
|
||||
$$
|
||||
\frac{\partial loss}{\partial Z1}=dZ2 \cdot W2^{\top} \odot dA1 \rightarrow dZ1
|
||||
$$
|
||||
$$
|
||||
dW1=X^{\top} \cdot dZ1
|
||||
$$
|
||||
$$
|
||||
dB1=dZ1
|
||||
$$
|
||||
|
||||
### 11.1.4 代码实现
|
||||
|
||||
绝大部分代码都在`HelperClass2`目录中的基本类实现,这里只有主过程:
|
||||
|
||||
```Python
|
||||
if __name__ == '__main__':
|
||||
......
|
||||
n_input = dataReader.num_feature
|
||||
n_hidden = 3
|
||||
n_output = dataReader.num_category
|
||||
eta, batch_size, max_epoch = 0.1, 10, 5000
|
||||
eps = 0.1
|
||||
hp = HyperParameters2(n_input, n_hidden, n_output, eta, max_epoch, batch_size, eps, NetType.MultipleClassifier, InitialMethod.Xavier)
|
||||
# create net and train
|
||||
net = NeuralNet2(hp, "Bank_233")
|
||||
net.train(dataReader, 100, True)
|
||||
net.ShowTrainingTrace()
|
||||
# show result
|
||||
......
|
||||
```
|
||||
|
||||
过程描述:
|
||||
|
||||
1. 读取数据文件
|
||||
2. 显示原始数据样本分布图
|
||||
3. 其它数据操作:归一化、打乱顺序、建立验证集
|
||||
4. 设置超参
|
||||
5. 建立神经网络开始训练
|
||||
6. 显示训练结果
|
||||
|
||||
### 11.1.5 运行结果
|
||||
|
||||
训练过程如图11-5所示。
|
||||
|
||||
<img src="./img/11/loss.png" />
|
||||
|
||||
图11-5 训练过程中的损失函数值和准确率值的变化
|
||||
|
||||
迭代了5000次,没有到达损失函数小于0.1的条件。
|
||||
|
||||
分类结果如图11-6所示。
|
||||
|
||||
<img src="./img/11/result.png" ch="500" />
|
||||
|
||||
图11-6 分类效果图
|
||||
|
||||
因为没达到精度要求,所以分类效果一般。从分类结果图上看,外圈圆形差不多拟合住了,但是内圈的方形还差很多。
|
||||
|
||||
打印输出:
|
||||
|
||||
```
|
||||
......
|
||||
epoch=4999, total_iteration=449999
|
||||
loss_train=0.225935, accuracy_train=0.800000
|
||||
loss_valid=0.137970, accuracy_valid=0.960000
|
||||
W= [[ -8.30315494 9.98115605 0.97148346]
|
||||
[ -5.84460922 -4.09908698 -11.18484376]]
|
||||
B= [[ 4.85763475 -5.61827538 7.94815347]]
|
||||
W= [[-32.28586038 -8.60177788 41.51614172]
|
||||
[-33.68897413 -7.93266621 42.09333288]
|
||||
[ 34.16449693 7.93537692 -41.19340947]]
|
||||
B= [[-11.11937314 3.45172617 7.66764697]]
|
||||
testing...
|
||||
0.952
|
||||
```
|
||||
最后的测试分类准确率为0.952。
|
||||
|
||||
### 代码位置
|
||||
|
||||
ch11, Level1
|
||||
|
|
|
@ -3,73 +3,12 @@
|
|||
|
||||
## 11.2 非线性多分类的工作原理
|
||||
|
||||
以下为本小节目录,详情请参阅《智能之门》正版图书,高等教育出版社。
|
||||
|
||||
### 11.2.1 隐层神经元数量的影响
|
||||
|
||||
下面列出了隐层神经元为2、4、8、16、32、64的情况,设定好最大epoch数为10000,以比较它们各自能达到的最大精度值的区别。每个配置只测试一轮,所以测出来的数据有一定的随机性。
|
||||
|
||||
表11-3展示了隐层神经元数与分类结果的关系。
|
||||
|
||||
表11-3 神经元数与网络能力及分类结果的关系
|
||||
|
||||
|神经元数|损失函数|分类结果|
|
||||
|---|---|---|
|
||||
|2|<img src='./img/11/loss_n2.png'/>|<img src='./img/11/result_n2.png'/>|
|
||||
||测试集准确度0.618,耗时49秒,损失函数值0.795。类似这种曲线的情况,损失函数值降不下去,准确度值升不上去,主要原因是网络能力不够。|没有完成分类任务|
|
||||
|4|<img src='./img/11/loss_n4.png'/>|<img src='./img/11/result_n4.png'/>|
|
||||
||测试准确度0.954,耗时51秒,损失函数值0.132。虽然可以基本完成分类任务,网络能力仍然不够。|基本完成,但是边缘不够清晰|
|
||||
|8|<img src='./img/11/loss_n8.png'/>|<img src='./img/11/result_n8.png'/>|
|
||||
||测试准确度0.97,耗时52秒,损失函数值0.105。可以先试试在后期衰减学习率,如果再训练5000轮没有改善的话,可以考虑增加网络能力。|基本完成,但是边缘不够清晰|
|
||||
|16|<img src='./img/11/loss_n16.png'/>|<img src='./img/11/result_n16.png'/>|
|
||||
||测试准确度0.978,耗时53秒,损失函数值0.094。同上,可以同时试着使用优化算法,看看是否能收敛更快。|较好地完成了分类任务|
|
||||
|32|<img src='./img/11/loss_n32.png'/>|<img src='./img/11/result_n32.png'/>|
|
||||
||测试准确度0.974,耗时53秒,损失函数值0.085。网络能力够了,从损失值下降趋势和准确度值上升趋势来看,可能需要更多的迭代次数。|较好地完成了分类任务|
|
||||
|64|<img src='./img/11/loss_n64.png'/>|<img src='./img/11/result_n64.png'/>|
|
||||
||测试准确度0.972,耗时64秒,损失函数值0.075。网络能力足够。|较好地完成了分类任务|
|
||||
|
||||
### 11.2.2 三维空间内的变换过程
|
||||
|
||||
从以上的比较中可知,隐层必须用3个神经元以上。在这个例子中,使用3个神经元能完成基本的分类任务,但精度要差一些。但是如果必须使用更多的神经元才能达到基本要求的话,我们将无法进行下一步的试验,所以,这个例子的难度对我们来说恰到好处。
|
||||
|
||||
使用10.6节学习的知识,如果隐层是两个神经元,我们可以把两个神经元的输出数据看作横纵坐标,在二维平面上绘制中间结果;由于使用了3个神经元,使得隐层的输出结果为每个样本一行三列,我们必须在三维空间中绘制中间结果。
|
||||
|
||||
```Python
|
||||
def Show3D(net, dr):
|
||||
......
|
||||
```
|
||||
|
||||
上述代码首先使用测试集(500个样本点)在已经训练好的网络上做一次推理,得到了隐层的计算结果,然后分别用`net.Z1`和`net.A1`的三列数据做为XYZ坐标值,绘制三维点图,列在表11-4中做比较。
|
||||
|
||||
表11-4 工作原理可视化
|
||||
|
||||
||正视角|侧视角|
|
||||
|---|---|---|
|
||||
|z1|<img src='./img/11/bank_z1_1.png'/>|<img src='./img/11/bank_z1_2.png'/>|
|
||||
||通过线性变换得到在三维空间中的线性平面|从侧面看的线性平面|
|
||||
|a1|<img src='./img/11/bank_a1_1.png'/>|<img src='./img/11/bank_a1_2.png'/>|
|
||||
||通过激活函数的非线性变化,被空间挤压成三角形|从侧面看三种颜色分成了三层|
|
||||
|
||||
`net.Z1`的点图的含义是,输入数据经过线性变换后的结果,可以看到由于只是线性变换,所以从侧视角看还只是一个二维平面的样子。
|
||||
|
||||
`net.A1`的点图含义是,经过激活函数做非线性变换后的图。由于绿色点比较靠近边缘,所以三维坐标中的每个值在经过Sigmoid激活函数计算后,都有至少一维坐标会是向1靠近的值,所以分散的比较开,形成外围的三角区域;蓝色点正好相反,三维坐标值都趋近于0,所以最后都集中在三维坐标原点的三角区域内;红色点处于前两者之间,因为有很多中间值。
|
||||
|
||||
再观察net.A1的侧视图,似乎是已经分层了,蓝点沉积下去,绿点浮上来,红点在中间,像鸡尾酒一样分成了三层,这就给第二层神经网络创造了做一个线性三分类的条件,只需要两个平面,就可以把三者轻松分开了。
|
||||
|
||||
#### 3D分类结果图
|
||||
|
||||
更高维的空间无法展示,所以当隐层神经元数量为4或8或更多时,基本无法理解空间变换的样子了。但是有一个方法可以近似地解释高维情况:在三维空间时,蓝色点会被推挤到一个角落形成一个三角形,那么在N(N>3)维空间中,蓝色点也会被推挤到一个角落。由于N很大,所以一个多边形会近似成一个圆形,也就是我们下面要生成的这些立体图的样子。
|
||||
|
||||
我们延续9.2节的3D效果体验,但是,多分类的实际实现方式是1对多的,所以我们只能一次显示一个类别的分类效果图,列在表11-5中。
|
||||
|
||||
表11-5 分类效果图
|
||||
|
||||
|||
|
||||
|---|---|
|
||||
|<img src='./img/11/multiple_3d_c1_1.png'/>|<img src='./img/11/multiple_3d_c2_1.png'/>|
|
||||
|红色:类别1样本区域|红色:类别2样本区域|
|
||||
|<img src='./img/11/multiple_3d_c3_1.png'/>|<img src='./img/11/multiple_3d_c1_c2_1.png'/>|
|
||||
|红色:类别3样本区域|红色:类别1,青色:类别2,紫色:类别3|
|
||||
|
||||
上表中最后一行的图片显示类别1和2的累加效果。由于最后的结果都被Softmax归一为$[0,1]$之间,所以我们可以简单地把类别1的数据乘以2,再加上类别2的数据即可。
|
||||
|
||||
### 代码位置
|
||||
|
||||
|
|
|
@ -3,81 +3,9 @@
|
|||
|
||||
## 11.3 分类样本不平衡问题
|
||||
|
||||
以下为本小节目录,详情请参阅《智能之门》正版图书,高等教育出版社。
|
||||
|
||||
### 11.3.1 什么是样本不平衡
|
||||
|
||||
英文名叫做Imbalanced Data。
|
||||
|
||||
在一般的分类学习方法中都有一个假设,就是不同类别的训练样本的数量相对平衡。
|
||||
|
||||
以二分类为例,比如正负例都各有1000个左右。如果是1200:800的比例,也是可以接受的,但是如果是1900:100,就需要有些措施来解决不平衡问题了,否则最后的训练结果很大可能是忽略了负例,将所有样本都分类为正类了。
|
||||
|
||||
如果是三分类,假设三个类别的样本比例为:1000:800:600,这是可以接受的;但如果是1000:300:100,就属于不平衡了。它带来的结果是分类器对第一类样本过拟合,而对其它两个类别的样本欠拟合,测试效果一定很糟糕。
|
||||
|
||||
类别不均衡问题是现实中很常见的问题,大部分分类任务中,各类别下的数据个数基本上不可能完全相等,但是一点点差异是不会产生任何影响与问题的。在现实中有很多类别不均衡问题,它是常见的,并且也是合理的,符合人们期望的。
|
||||
|
||||
在前面,我们使用准确度这个指标来评价分类质量,可以看出,在类别不均衡时,准确度这个评价指标并不能work。比如一个极端的例子是,在疾病预测时,有98个正例,2个反例,那么分类器只要将所有样本预测为正类,就可以得到98%的准确度,则此分类器就失去了价值。
|
||||
|
||||
### 11.3.2 如何解决样本不平衡问题
|
||||
|
||||
#### 平衡数据集
|
||||
|
||||
有一句话叫做“更多的数据往往战胜更好的算法”。所以一定要先想办法扩充样本数量少的类别的数据,比如目前的正负类样本数量是1000:100,则可以再搜集2000个数据,最后得到了2800:300的比例,此时可以从正类样本中丢弃一些,变成500:300,就可以训练了。
|
||||
|
||||
一些经验法则:
|
||||
|
||||
- 考虑对大类下的样本(超过1万、十万甚至更多)进行欠采样,即删除部分样本;
|
||||
- 考虑对小类下的样本(不足1万甚至更少)进行过采样,即添加部分样本的副本;
|
||||
- 考虑尝试随机采样与非随机采样两种采样方法;
|
||||
- 考虑对各类别尝试不同的采样比例,比一定是1:1,有时候1:1反而不好,因为与现实情况相差甚远;
|
||||
- 考虑同时使用过采样(over-sampling)与欠采样(under-sampling)。
|
||||
|
||||
#### 尝试其它评价指标
|
||||
|
||||
从前面的分析可以看出,准确度这个评价指标在类别不均衡的分类任务中并不能work,甚至进行误导(分类器不work,但是从这个指标来看,该分类器有着很好的评价指标得分)。因此在类别不均衡分类任务中,需要使用更有说服力的评价指标来对分类器进行评价。如何对不同的问题选择有效的评价指标参见这里。
|
||||
|
||||
常规的分类评价指标可能会失效,比如将所有的样本都分类成大类,那么准确率、精确率等都会很高。这种情况下,AUC是最好的评价指标。
|
||||
|
||||
#### 尝试产生人工数据样本
|
||||
|
||||
一种简单的人工样本数据产生的方法便是,对该类下的所有样本每个属性特征的取值空间中随机选取一个组成新的样本,即属性值随机采样。你可以使用基于经验对属性值进行随机采样而构造新的人工样本,或者使用类似朴素贝叶斯方法假设各属性之间互相独立进行采样,这样便可得到更多的数据,但是无法保证属性之前的线性关系(如果本身是存在的)。
|
||||
|
||||
有一个系统的构造人工数据样本的方法SMOTE(Synthetic Minority Over-sampling Technique)。SMOTE是一种过采样算法,它构造新的小类样本而不是产生小类中已有的样本的副本,即该算法构造的数据是新样本,原数据集中不存在的。该基于距离度量选择小类别下两个或者更多的相似样本,然后选择其中一个样本,并随机选择一定数量的邻居样本对选择的那个样本的一个属性增加噪声,每次处理一个属性。这样就构造了更多的新生数据。具体可以参见原始论文。
|
||||
|
||||
使用命令:
|
||||
|
||||
```
|
||||
pip install imblearn
|
||||
```
|
||||
可以安装SMOTE算法包,用于实现样本平衡。
|
||||
|
||||
#### 尝试一个新的角度理解问题
|
||||
|
||||
我们可以从不同于分类的角度去解决数据不均衡性问题,我们可以把那些小类的样本作为异常点(outliers),因此该问题便转化为异常点检测(anomaly detection)与变化趋势检测问题(change detection)。
|
||||
|
||||
异常点检测即是对那些罕见事件进行识别。如通过机器的部件的振动识别机器故障,又如通过系统调用序列识别恶意程序。这些事件相对于正常情况是很少见的。
|
||||
|
||||
变化趋势检测类似于异常点检测,不同在于其通过检测不寻常的变化趋势来识别。如通过观察用户模式或银行交易来检测用户行为的不寻常改变。
|
||||
|
||||
将小类样本作为异常点这种思维的转变,可以帮助考虑新的方法去分离或分类样本。这两种方法从不同的角度去思考,让你尝试新的方法去解决问题。
|
||||
|
||||
#### 修改现有算法
|
||||
|
||||
- 设超大类中样本的个数是极小类中样本个数的L倍,那么在随机梯度下降(SGD,stochastic gradient descent)算法中,每次遇到一个极小类中样本进行训练时,训练L次。
|
||||
- 将大类中样本划分到L个聚类中,然后训练L个分类器,每个分类器使用大类中的一个簇与所有的小类样本进行训练得到。最后对这L个分类器采取少数服从多数对未知类别数据进行分类,如果是连续值(预测),那么采用平均值。
|
||||
- 设小类中有N个样本。将大类聚类成N个簇,然后使用每个簇的中心组成大类中的N个样本,加上小类中所有的样本进行训练。
|
||||
|
||||
无论你使用前面的何种方法,都对某个或某些类进行了损害。为了不进行损害,那么可以使用全部的训练集采用多种分类方法分别建立分类器而得到多个分类器,采用投票的方式对未知类别的数据进行分类,如果是连续值(预测),那么采用平均值。
|
||||
在最近的ICML论文中,表明增加数据量使得已知分布的训练集的误差增加了,即破坏了原有训练集的分布,从而可以提高分类器的性能。这篇论文与类别不平衡问题不相关,因为它隐式地使用数学方式增加数据而使得数据集大小不变。但是,我认为破坏原有的分布是有益的。
|
||||
|
||||
#### 集成学习
|
||||
|
||||
一个很好的方法去处理非平衡数据问题,并且在理论上证明了。这个方法便是由Robert E. Schapire于1990年在Machine Learning提出的”The strength of weak learnability” ,该方法是一个boosting算法,它递归地训练三个弱学习器,然后将这三个弱学习器结合起形成一个强的学习器。我们可以使用这个算法的第一步去解决数据不平衡问题。
|
||||
|
||||
1. 首先使用原始数据集训练第一个学习器L1;
|
||||
2. 然后使用50%在L1学习正确和50%学习错误的那些样本训练得到学习器L2,即从L1中学习错误的样本集与学习正确的样本集中,循环一边采样一个;
|
||||
3. 接着,使用L1与L2不一致的那些样本去训练得到学习器L3;
|
||||
4. 最后,使用投票方式作为最后输出。
|
||||
|
||||
那么如何使用该算法来解决类别不平衡问题呢?
|
||||
|
||||
假设是一个二分类问题,大部分的样本都是true类。让L1输出始终为true。使用50%在L1分类正确的与50%分类错误的样本训练得到L2,即从L1中学习错误的样本集与学习正确的样本集中,循环一边采样一个。因此,L2的训练样本是平衡的。L使用L1与L2分类不一致的那些样本训练得到L3,即在L2中分类为false的那些样本。最后,结合这三个分类器,采用投票的方式来决定分类结果,因此只有当L2与L3都分类为false时,最终结果才为false,否则true。
|
||||
|
|
|
@ -3,302 +3,16 @@
|
|||
|
||||
## 12.1 三层神经网络的实现
|
||||
|
||||
以下为本小节目录,详情请参阅《智能之门》正版图书,高等教育出版社。
|
||||
|
||||
### 12.1.1 定义神经网络
|
||||
|
||||
为了完成MNIST分类,我们需要设计一个三层神经网络结构,如图12-2所示。
|
||||
|
||||
<img src="./img/12/nn3.png" ch="500" />
|
||||
|
||||
图12-2 三层神经网络结构
|
||||
|
||||
#### 输入层
|
||||
|
||||
共计$28\times 28=784$个特征值:
|
||||
|
||||
$$
|
||||
X=\begin{pmatrix}
|
||||
x_1 & x_2 & \cdots & x_{784}
|
||||
\end{pmatrix}
|
||||
$$
|
||||
|
||||
#### 隐层1
|
||||
|
||||
- 权重矩阵$W1$形状为$784\times 64$
|
||||
|
||||
$$
|
||||
W1=\begin{pmatrix}
|
||||
w1_{1,1} & w1_{1,2} & \cdots & w1_{1,64} \\\\
|
||||
\vdots & \vdots & \cdots & \vdots \\\\
|
||||
w1_{784,1} & w1_{784,2} & \cdots & w1_{784,64}
|
||||
\end{pmatrix}
|
||||
$$
|
||||
|
||||
- 偏移矩阵$B1$的形状为$1\times 64$
|
||||
|
||||
$$
|
||||
B1=\begin{pmatrix}
|
||||
b1_{1} & b1_{2} & \cdots & b1_{64}
|
||||
\end{pmatrix}
|
||||
$$
|
||||
|
||||
- 隐层1由64个神经元构成,其结果为$1\times 64$的矩阵
|
||||
|
||||
$$
|
||||
Z1=\begin{pmatrix}
|
||||
z1_{1} & z1_{2} & \cdots & z1_{64}
|
||||
\end{pmatrix}
|
||||
$$
|
||||
$$
|
||||
A1=\begin{pmatrix}
|
||||
a1_{1} & a1_{2} & \cdots & a1_{64}
|
||||
\end{pmatrix}
|
||||
$$
|
||||
|
||||
#### 隐层2
|
||||
|
||||
- 权重矩阵$w2$形状为$64\times 16$
|
||||
|
||||
$$
|
||||
W2=\begin{pmatrix}
|
||||
w2_{1,1} & w2_{1,2} & \cdots & w2_{1,16} \\\\
|
||||
\vdots & \vdots & \cdots & \vdots \\\\
|
||||
w2_{64,1} & w2_{64,2} & \cdots & w2_{64,16}
|
||||
\end{pmatrix}
|
||||
$$
|
||||
|
||||
- 偏移矩阵#B2#的形状是$1\times 16$
|
||||
|
||||
$$
|
||||
B2=\begin{pmatrix}
|
||||
b2_{1} & b2_{2} & \cdots & b2_{16}
|
||||
\end{pmatrix}
|
||||
$$
|
||||
|
||||
- 隐层2由16个神经元构成
|
||||
|
||||
$$
|
||||
Z2=\begin{pmatrix}
|
||||
z2_{1} & z2_{2} & \cdots & z2_{16}
|
||||
\end{pmatrix}
|
||||
$$
|
||||
$$
|
||||
A2=\begin{pmatrix}
|
||||
a2_{1} & a2_{2} & \cdots & a2_{16}
|
||||
\end{pmatrix}
|
||||
$$
|
||||
|
||||
#### 输出层
|
||||
|
||||
- 权重矩阵$W3$的形状为$16\times 10$
|
||||
|
||||
$$
|
||||
W3=\begin{pmatrix}
|
||||
w3_{1,1} & w3_{1,2} & \cdots & w3_{1,10} \\\\
|
||||
\vdots & \vdots & \cdots & \vdots \\\\
|
||||
w3_{16,1} & w3_{16,2} & \cdots & w3_{16,10}
|
||||
\end{pmatrix}
|
||||
$$
|
||||
|
||||
- 输出层的偏移矩阵$B3$的形状是$1\times 10$
|
||||
|
||||
$$
|
||||
B3=\begin{pmatrix}
|
||||
b3_{1}& b3_{2} & \cdots & b3_{10}
|
||||
\end{pmatrix}
|
||||
$$
|
||||
|
||||
- 输出层有10个神经元使用Softmax函数进行分类
|
||||
|
||||
$$
|
||||
Z3=\begin{pmatrix}
|
||||
z3_{1} & z3_{2} & \cdots & z3_{10}
|
||||
\end{pmatrix}
|
||||
$$
|
||||
$$
|
||||
A3=\begin{pmatrix}
|
||||
a3_{1} & a3_{2} & \cdots & a3_{10}
|
||||
\end{pmatrix}
|
||||
$$
|
||||
|
||||
### 12.1.2 前向计算
|
||||
|
||||
我们都是用大写符号的矩阵形式的公式来描述,在每个矩阵符号的右上角是其形状。
|
||||
|
||||
#### 隐层1
|
||||
|
||||
$$Z1 = X \cdot W1 + B1 \tag{1}$$
|
||||
|
||||
$$A1 = Sigmoid(Z1) \tag{2}$$
|
||||
|
||||
#### 隐层2
|
||||
|
||||
$$Z2 = A1 \cdot W2 + B2 \tag{3}$$
|
||||
|
||||
$$A2 = Tanh(Z2) \tag{4}$$
|
||||
|
||||
#### 输出层
|
||||
|
||||
$$Z3 = A2 \cdot W3 + B3 \tag{5}$$
|
||||
|
||||
$$A3 = Softmax(Z3) \tag{6}$$
|
||||
|
||||
我们的约定是行为样本,列为一个样本的所有特征,这里是784个特征,因为图片高和宽均为28,总共784个点,把每一个点的值做为特征向量。
|
||||
|
||||
两个隐层,分别定义64个神经元和16个神经元。第一个隐层用Sigmoid激活函数,第二个隐层用Tanh激活函数。
|
||||
|
||||
输出层10个神经元,再加上一个Softmax计算,最后有$a1,a2,...a10$共十个输出,分别代表0-9的10个数字。
|
||||
|
||||
### 12.1.3 反向传播
|
||||
|
||||
和以前的两层网络没有多大区别,只不过多了一层,而且用了tanh激活函数,目的是想把更多的梯度值回传,因为tanh函数比sigmoid函数稍微好一些,比如原点对称,零点梯度值大。
|
||||
|
||||
#### 输出层
|
||||
|
||||
$$dZ3 = A3-Y \tag{7}$$
|
||||
$$dW3 = A2^{\top} \cdot dZ3 \tag{8}$$
|
||||
$$dB3=dZ3 \tag{9}$$
|
||||
|
||||
#### 隐层2
|
||||
|
||||
$$dA2 = dZ3 \cdot W3^{\top} \tag{10}$$
|
||||
$$dZ2 = dA2 \odot (1-A2 \odot A2) \tag{11}$$
|
||||
$$dW2 = A1^{\top} \cdot dZ2 \tag{12}$$
|
||||
$$dB2 = dZ2 \tag{13}$$
|
||||
|
||||
#### 隐层1
|
||||
|
||||
$$dA1 = dZ2 \cdot W2^{\top} \tag{14}$$
|
||||
$$dZ1 = dA1 \odot A1 \odot (1-A1) \tag{15}$$
|
||||
$$dW1 = X^{\top} \cdot dZ1 \tag{16}$$
|
||||
$$dB1 = dZ1 \tag{17}$$
|
||||
|
||||
### 12.1.4 代码实现
|
||||
|
||||
在`HelperClass3` / `NeuralNet3.py`中,下面主要列出与两层网络不同的代码。
|
||||
|
||||
#### 初始化
|
||||
|
||||
```Python
|
||||
class NeuralNet3(object):
|
||||
def __init__(self, hp, model_name):
|
||||
...
|
||||
self.wb1 = WeightsBias(self.hp.num_input, self.hp.num_hidden1, self.hp.init_method, self.hp.eta)
|
||||
self.wb1.InitializeWeights(self.subfolder, False)
|
||||
self.wb2 = WeightsBias(self.hp.num_hidden1, self.hp.num_hidden2, self.hp.init_method, self.hp.eta)
|
||||
self.wb2.InitializeWeights(self.subfolder, False)
|
||||
self.wb3 = WeightsBias(self.hp.num_hidden2, self.hp.num_output, self.hp.init_method, self.hp.eta)
|
||||
self.wb3.InitializeWeights(self.subfolder, False)
|
||||
```
|
||||
初始化部分需要构造三组`WeightsBias`对象,请注意各组的输入输出数量,决定了矩阵的形状。
|
||||
|
||||
#### 前向计算
|
||||
|
||||
```Python
|
||||
def forward(self, batch_x):
|
||||
# 公式1
|
||||
self.Z1 = np.dot(batch_x, self.wb1.W) + self.wb1.B
|
||||
# 公式2
|
||||
self.A1 = Sigmoid().forward(self.Z1)
|
||||
# 公式3
|
||||
self.Z2 = np.dot(self.A1, self.wb2.W) + self.wb2.B
|
||||
# 公式4
|
||||
self.A2 = Tanh().forward(self.Z2)
|
||||
# 公式5
|
||||
self.Z3 = np.dot(self.A2, self.wb3.W) + self.wb3.B
|
||||
# 公式6
|
||||
if self.hp.net_type == NetType.BinaryClassifier:
|
||||
self.A3 = Logistic().forward(self.Z3)
|
||||
elif self.hp.net_type == NetType.MultipleClassifier:
|
||||
self.A3 = Softmax().forward(self.Z3)
|
||||
else: # NetType.Fitting
|
||||
self.A3 = self.Z3
|
||||
#end if
|
||||
self.output = self.A3
|
||||
```
|
||||
前向计算部分增加了一层,并且使用`Tanh()`作为激活函数。
|
||||
|
||||
- 反向传播
|
||||
```Python
|
||||
def backward(self, batch_x, batch_y, batch_a):
|
||||
# 批量下降,需要除以样本数量,否则会造成梯度爆炸
|
||||
m = batch_x.shape[0]
|
||||
|
||||
# 第三层的梯度输入 公式7
|
||||
dZ3 = self.A3 - batch_y
|
||||
# 公式8
|
||||
self.wb3.dW = np.dot(self.A2.T, dZ3)/m
|
||||
# 公式9
|
||||
self.wb3.dB = np.sum(dZ3, axis=0, keepdims=True)/m
|
||||
|
||||
# 第二层的梯度输入 公式10
|
||||
dA2 = np.dot(dZ3, self.wb3.W.T)
|
||||
# 公式11
|
||||
dZ2,_ = Tanh().backward(None, self.A2, dA2)
|
||||
# 公式12
|
||||
self.wb2.dW = np.dot(self.A1.T, dZ2)/m
|
||||
# 公式13
|
||||
self.wb2.dB = np.sum(dZ2, axis=0, keepdims=True)/m
|
||||
|
||||
# 第一层的梯度输入 公式8
|
||||
dA1 = np.dot(dZ2, self.wb2.W.T)
|
||||
# 第一层的dZ 公式10
|
||||
dZ1,_ = Sigmoid().backward(None, self.A1, dA1)
|
||||
# 第一层的权重和偏移 公式11
|
||||
self.wb1.dW = np.dot(batch_x.T, dZ1)/m
|
||||
self.wb1.dB = np.sum(dZ1, axis=0, keepdims=True)/m
|
||||
|
||||
def update(self):
|
||||
self.wb1.Update()
|
||||
self.wb2.Update()
|
||||
self.wb3.Update()
|
||||
```
|
||||
反向传播也相应地增加了一层,注意要用对应的`Tanh()`的反向公式。梯度更新时也是三组权重值同时更新。
|
||||
|
||||
- 主过程
|
||||
|
||||
```Python
|
||||
if __name__ == '__main__':
|
||||
......
|
||||
n_input = dataReader.num_feature
|
||||
n_hidden1 = 64
|
||||
n_hidden2 = 16
|
||||
n_output = dataReader.num_category
|
||||
eta = 0.2
|
||||
eps = 0.01
|
||||
batch_size = 128
|
||||
max_epoch = 40
|
||||
|
||||
hp = HyperParameters3(n_input, n_hidden1, n_hidden2, n_output, eta, max_epoch, batch_size, eps, NetType.MultipleClassifier, InitialMethod.Xavier)
|
||||
net = NeuralNet3(hp, "MNIST_64_16")
|
||||
net.train(dataReader, 0.5, True)
|
||||
net.ShowTrainingTrace(xline="iteration")
|
||||
```
|
||||
超参配置:第一隐层64个神经元,第二隐层16个神经元,学习率0.2,批大小128,Xavier初始化,最大训练40个epoch。
|
||||
|
||||
### 12.1.5 运行结果
|
||||
|
||||
损失函数值和准确度值变化曲线如图12-3。
|
||||
|
||||
<img src="./img/12/loss.png" />
|
||||
|
||||
图12-3 训练过程中损失函数和准确度的变化
|
||||
|
||||
打印输出部分:
|
||||
|
||||
```
|
||||
...
|
||||
epoch=38, total_iteration=16769
|
||||
loss_train=0.012860, accuracy_train=1.000000
|
||||
loss_valid=0.100281, accuracy_valid=0.969400
|
||||
epoch=39, total_iteration=17199
|
||||
loss_train=0.006867, accuracy_train=1.000000
|
||||
loss_valid=0.098164, accuracy_valid=0.971000
|
||||
time used: 25.697904109954834
|
||||
testing...
|
||||
0.9749
|
||||
```
|
||||
|
||||
在测试集上得到的准确度为97.49%,比较理想。
|
||||
|
||||
### 代码位置
|
||||
|
||||
|
|
|
@ -3,190 +3,17 @@
|
|||
|
||||
## 12.2 梯度检查
|
||||
|
||||
以下为本小节目录,详情请参阅《智能之门》正版图书,高等教育出版社。
|
||||
|
||||
### 12.2.1 为何要做梯度检查?
|
||||
|
||||
神经网络算法使用反向传播计算目标函数关于每个参数的梯度,可以看做解析梯度。由于计算过程中涉及到的参数很多,用代码实现的反向传播计算的梯度很容易出现误差,导致最后迭代得到效果很差的参数值。
|
||||
|
||||
为了确认代码中反向传播计算的梯度是否正确,可以采用梯度检验(gradient check)的方法。通过计算数值梯度,得到梯度的近似值,然后和反向传播得到的梯度进行比较,若两者相差很小的话则证明反向传播的代码是正确无误的。
|
||||
|
||||
### 12.2.2 数值微分
|
||||
|
||||
#### 导数概念回忆
|
||||
|
||||
$$
|
||||
f'(x)=\lim_{h \to 0} \frac{f(x+h)-f(x)}{h} \tag{1}
|
||||
$$
|
||||
|
||||
其含义就是$x$的微小变化$h$($h$为无限小的值),会导致函数$f(x)$的值有多大变化。在`Python`中可以这样实现:
|
||||
|
||||
```Python
|
||||
def numerical_diff(f, x):
|
||||
h = 1e-5
|
||||
d = (f(x+h) - f(x))/h
|
||||
return d
|
||||
```
|
||||
|
||||
因为计算机的舍入误差的原因,`h`不能太小,比如`1e-10`,会造成计算结果上的误差,所以我们一般用`[1e-4,1e-7]`之间的数值。
|
||||
|
||||
但是如果使用上述方法会有一个问题,如图12-4所示。
|
||||
|
||||
<img src="./img/12/grad_check.png" ch="500" />
|
||||
|
||||
图12-4 数值微分方法
|
||||
|
||||
红色实线为真实的导数切线,蓝色虚线是上述方法的体现,即从$x$到$x+h$画一条直线,来模拟真实导数。但是可以明显看出红色实线和蓝色虚线的斜率是不等的。因此我们通常用绿色的虚线来模拟真实导数,公式变为:
|
||||
|
||||
$$
|
||||
f'(x) = \lim_{h \to 0} \frac{f(x+h)-f(x-h)}{2h} \tag{2}
|
||||
$$
|
||||
|
||||
公式2被称为双边逼近方法。
|
||||
|
||||
用双边逼近形式会比单边逼近形式的误差小100~10000倍左右,可以用泰勒展开来证明。
|
||||
|
||||
#### 泰勒公式
|
||||
|
||||
泰勒公式是将一个在$x=x_0$处具有n阶导数的函数$f(x)$利用关于$(x-x_0)$的n次多项式来逼近函数的方法。若函数$f(x)$在包含$x_0$的某个闭区间$[a,b]$上具有n阶导数,且在开区间$(a,b)$上具有$n+1$阶导数,则对闭区间$[a,b]$上任意一点$x$,下式成立:
|
||||
|
||||
$$f(x)=\frac{f(x_0)}{0!} + \frac{f'(x_0)}{1!}(x-x_0)+\frac{f''(x_0)}{2!}(x-x_0)^2 + ...+\frac{f^{(n)}(x_0)}{n!}(x-x_0)^n+R_n(x) \tag{3}$$
|
||||
|
||||
其中,$f^{(n)}(x)$表示$f(x)$的$n$阶导数,等号后的多项式称为函数$f(x)$在$x_0$处的泰勒展开式,剩余的$R_n(x)$是泰勒公式的余项,是$(x-x_0)^n$的高阶无穷小。
|
||||
|
||||
利用泰勒展开公式,令$x=\theta + h, x_0=\theta$,我们可以得到:
|
||||
|
||||
$$f(\theta + h)=f(\theta) + f'(\theta)h + O(h^2) \tag{4}$$
|
||||
|
||||
#### 单边逼近误差
|
||||
|
||||
如果用单边逼近,把公式4两边除以$h$后变形:
|
||||
|
||||
$$f'(\theta) + O(h)=\frac{f(\theta+h)-f(\theta)}{h} \tag{5}$$
|
||||
|
||||
公式5已经和公式1的定义非常接近了,只是左侧多出来的第二项,就是逼近的误差,是个$O(h)$级别的误差项。
|
||||
|
||||
#### 双边逼近误差
|
||||
|
||||
如果用双边逼近,我们用三阶泰勒展开:
|
||||
|
||||
令$x=\theta + h, x_0=\theta$,我们可以得到:
|
||||
|
||||
$$f(\theta + h)=f(\theta) + f'(\theta)h + f''(\theta)h^2 + O(h^3) \tag{6}$$
|
||||
|
||||
再令$x=\theta - h, x_0=\theta$我们可以得到:
|
||||
|
||||
$$f(\theta - h)=f(\theta) - f'(\theta)h + f''(\theta)h^2 - O(h^3) \tag{7}$$
|
||||
|
||||
公式6减去公式7,有:
|
||||
|
||||
$$f(\theta + h) - f(\theta - h)=2f'(\theta)h + 2O(h^3) \tag{8}$$
|
||||
|
||||
两边除以$2h$:
|
||||
|
||||
$$f'(\theta) + O(h^2)={f(\theta + h) - f(\theta - h) \over 2h} \tag{9}$$
|
||||
|
||||
公式9中,左侧多出来的第二项,就是双边逼近的误差,是个$O(h^2)$级别的误差项,比公式5中的误差项小很多数量级。
|
||||
|
||||
### 12.2.3 实例说明
|
||||
|
||||
公式2就是梯度检查的理论基础。比如一个函数:
|
||||
|
||||
$$f(x) = x^2 + 3x$$
|
||||
|
||||
我们看一下它在$x=2$处的数值微分,令$h = 0.001$:
|
||||
|
||||
$$
|
||||
\begin{aligned}
|
||||
f(x+h) &= f(2+0.001) \\\\
|
||||
&= (2+0.001)^2 + 3 \times (2+0.001) \\\\
|
||||
&=10.007001
|
||||
\end{aligned}
|
||||
$$
|
||||
$$
|
||||
\begin{aligned}
|
||||
f(x-h) &= f(2-0.001) \\\\
|
||||
&= (2-0.001)^2 + 3 \times (2-0.001) \\\\
|
||||
&=9.993001
|
||||
\end{aligned}
|
||||
$$
|
||||
$$
|
||||
\frac{f(x+h)-f(x-h)}{2h}=\frac{10.007001-9.993001}{2 \times 0.001}=7 \tag{10}
|
||||
$$
|
||||
|
||||
再看它的数学解析解:
|
||||
|
||||
$$f'(x)=2x+3$$
|
||||
$$f'(x|_{x=2})=2 \times 2+3=7 \tag{11}$$
|
||||
|
||||
可以看到公式10和公式11的结果一致。当然在实际应用中,一般不可能会完全相等,只要两者的误差小于`1e-5`以下,我们就认为是满足精度要求的。
|
||||
|
||||
### 12.2.4 算法实现
|
||||
|
||||
在神经网络中,我们假设使用多分类的交叉熵函数,则其形式为:
|
||||
|
||||
$$J(w,b) =- \sum_{i=1}^m \sum_{j=1}^n y_{ij} \ln a_{ij}$$
|
||||
|
||||
m是样本数,n是分类数。
|
||||
|
||||
#### 参数向量化
|
||||
|
||||
我们需要检查的是关于$W$和$B$的梯度,而$W$和$B$是若干个矩阵,而不是一个标量,所以在进行梯度检验之前,我们先做好准备工作,那就是把矩阵$W$和$B$向量化,然后把神经网络中所有层的向量化的$W$和$B$连接在一起(concatenate),成为一个大向量,我们称之为$J(\theta)$,然后对通过back-prop过程得到的W和B求导的结果$d\theta_{real}$也做同样的变换,接下来我们就要开始做检验了。
|
||||
|
||||
向量化的$W,B$连接以后,统一称作为$\theta$,按顺序用不同下标区分,于是有$J(\theta)$的表达式为:
|
||||
|
||||
$$J(w,b)=J(\theta_1,...,\theta_i,...,\theta_n)$$
|
||||
|
||||
对于上式中的每一个向量,我们依次使用公式2的方式做检查,于是有对第i个向量值的梯度检查公式:
|
||||
|
||||
$$\frac{\partial J}{\partial \theta_i}=\frac{J(\theta_1,...,\theta_i+h,...,\theta_n) - J(\theta_1,...,\theta_i-h,...,\theta_n)}{2h}$$
|
||||
|
||||
因为我们是要比较两个向量的对应分量的差别,这个可以用对应分量差的平方和的开方(欧氏距离)来刻画。但是我们不希望得到一个具体的刻画差异的值,而是希望得到一个比率,这也便于我们得到一个标准的梯度检验的要求。
|
||||
|
||||
为什么这样说呢?其实我们可以这样想,假设刚开始的迭代,参数的梯度很大,而随着不断迭代直至收敛,参数的梯度逐渐趋近于0,即越来越小,这个过程中,分子(欧氏距离)是跟梯度的值有关的,随着迭代次数的增加,也会减小。那在迭代过程中,我们只利用分子就没有一个固定标准去判断梯度检验的效果,而加上一个分母,将梯度的平方和考虑进去,大值比大值,小值比小值,我们就可以得到一个比率,同样也可以得到一个确定的标准去衡量梯度检验的效果。
|
||||
|
||||
#### 算法
|
||||
|
||||
1. 初始化神经网络的所有矩阵参数(可以使用随机初始化或其它非0的初始化方法)
|
||||
2. 把所有层的$W,B$都转化成向量,按顺序存放在$\theta$中
|
||||
3. 随机设置$X$值,最好是归一化之后的值,在[0,1]之间
|
||||
4. 做一次前向计算,再紧接着做一次反向计算,得到各参数的梯度$d\theta_{real}$
|
||||
5. 把得到的梯度$d\theta_{real}$变化成向量形式,其尺寸应该和第2步中的$\theta$相同,且一一对应($W$对应$dW$, $B$对应$dB$)
|
||||
6. 对2中的$\theta$向量中的每一个值,做一次双边逼近,得到$d\theta_{approx}$
|
||||
7. 比较$d\theta_{real}$和$d\theta_{approx}$的值,通过计算两个向量之间的欧式距离:
|
||||
|
||||
$$diff = \frac{\parallel d\theta_{real} - d\theta_{approx}\parallel_2}{\parallel d\theta_{approx}\parallel_2 + \parallel d\theta_{real}\parallel_2}$$
|
||||
|
||||
结果判断:
|
||||
|
||||
1. $diff > 1e^{-2}$
|
||||
|
||||
梯度计算肯定出了问题。
|
||||
|
||||
2. $1e^{-2} > diff > 1e^{-4}$
|
||||
|
||||
可能有问题了,需要检查。
|
||||
|
||||
3. $1e^{-4} \gt diff \gt 1e^{-7}$
|
||||
|
||||
不光滑的激励函数来说时可以接受的,但是如果使用平滑的激励函数如 tanh nonlinearities and softmax,这个结果还是太高了。
|
||||
|
||||
4. $1e^{-7} \gt diff$
|
||||
|
||||
可以喝杯茶庆祝下。
|
||||
|
||||
另外要注意的是,随着网络深度的增加会使得误差积累,如果用了10层的网络,得到的相对误差为`1e-2`那么这个结果也是可以接受的。
|
||||
|
||||
### 12.2.5 注意事项
|
||||
|
||||
1. 首先,不要使用梯度检验去训练,即不要使用梯度检验方法去计算梯度,因为这样做太慢了,在训练过程中,我们还是使用backprop去计算参数梯度,而使用梯度检验去调试,去检验backprop的过程是否准确。
|
||||
|
||||
2. 其次,如果我们在使用梯度检验过程中发现backprop过程出现了问题,就需要对所有的参数进行计算,以判断造成计算偏差的来源在哪里,它可能是在求解$B$出现问题,也可能是在求解某一层的$W$出现问题,梯度检验可以帮助我们确定发生问题的范围,以帮助我们调试。
|
||||
|
||||
3. 别忘了正则化。如果我们添加了二范数正则化,在使用backprop计算参数梯度时,不要忘记梯度的形式已经发生了变化,要记得加上正则化部分,同理,在进行梯度检验时,也要记得目标函数$J$的形式已经发生了变化。
|
||||
|
||||
4. 注意,如果我们使用了drop-out正则化,梯度检验就不可用了。为什么呢?因为我们知道drop-out是按照一定的保留概率随机保留一些节点,因为它的随机性,目标函数$J$的形式变得非常不明确,这时我们便无法再用梯度检验去检验backprop。如果非要使用drop-out且又想检验backprop,我们可以先将保留概率设为1,即保留全部节点,然后用梯度检验来检验backprop过程,如果没有问题,我们再改变保留概率的值来应用drop-out。
|
||||
|
||||
5. 最后,介绍一种特别少见的情况。在刚开始初始化W和b时,W和b的值都还很小,这时backprop过程没有问题,但随着迭代过程的进行,$W$和$B$的值变得越来越大时,backprop过程可能会出现问题,且可能梯度差距越来越大。要避免这种情况,我们需要多进行几次梯度检验,比如在刚开始初始化权重时进行一次检验,在迭代一段时间之后,再使用梯度检验去验证backprop过程。
|
||||
|
||||
### 代码位置
|
||||
|
||||
ch12, Level2
|
||||
|
|
|
@ -3,295 +3,14 @@
|
|||
|
||||
## 12.3 学习率与批大小
|
||||
|
||||
在梯度下降公式中:
|
||||
|
||||
$$
|
||||
w_{t+1} = w_t - \frac{\eta}{m} \sum_i^m \nabla J(w,b) \tag{1}
|
||||
$$
|
||||
|
||||
其中,$\eta$是学习率,m是批大小。所以,学习率与批大小是对梯度下降影响最大的两个因子。
|
||||
以下为本小节目录,详情请参阅《智能之门》正版图书,高等教育出版社。
|
||||
|
||||
### 12.3.1 关于学习率的挑战
|
||||
|
||||
有一句业内人士的流传的话:如果所有超参中,只需要调整一个参数,那么就是学习率。由此可见学习率是多么的重要,如果读者仔细做了9.6的试验,将会发现,不论你改了批大小或是隐层神经元的数量,总会找到一个合适的学习率来适应上面的修改,最终得到理想的训练结果。
|
||||
|
||||
但是学习率是一个非常难调的参数,下面给出具体说明。
|
||||
|
||||
前面章节学习过,普通梯度下降法,包含三种形式:
|
||||
|
||||
1. 单样本
|
||||
2. 全批量样本
|
||||
3. 小批量样本
|
||||
|
||||
我们通常把1和3统称为SGD(Stochastic Gradient Descent)。当批量不是很大时,全批量也可以纳入此范围。大的含义是:万级以上的数据量。
|
||||
|
||||
使用梯度下降的这些形式时,我们通常面临以下挑战:
|
||||
|
||||
1. 很难选择出合适的学习率
|
||||
|
||||
太小的学习率会导致网络收敛过于缓慢,而学习率太大可能会影响收敛,并导致损失函数在最小值上波动,甚至出现梯度发散。
|
||||
|
||||
2. 相同的学习率并不适用于所有的参数更新
|
||||
|
||||
如果训练集数据很稀疏,且特征频率非常不同,则不应该将其全部更新到相同的程度,但是对于很少出现的特征,应使用更大的更新率。
|
||||
|
||||
3. 避免陷于多个局部最小值中。
|
||||
|
||||
实际上,问题并非源于局部最小值,而是来自鞍点,即一个维度向上倾斜且另一维度向下倾斜的点。这些鞍点通常被相同误差值的平面所包围,这使得SGD算法很难脱离出来,因为梯度在所有维度上接近于零。
|
||||
|
||||
表12-1 鞍点和驻点
|
||||
|
||||
|鞍点|驻点|
|
||||
|---|---|
|
||||
|<img src="..\Images\12\saddle_point.png" width="640">|<img src="..\Images\9\sgd_loss_8.png">|
|
||||
|
||||
表12-1中左图就是鞍点的定义,在鞍点附近,梯度下降算法经常会陷入泥潭,从而产生右图一样的历史记录曲线:有一段时间,Loss值随迭代次数缓慢下降,似乎在寻找突破口,然后忽然找到了,就一路下降,最终收敛。
|
||||
|
||||
为什么在3000至6000个epoch之间,有很大一段平坦地段,Loss值并没有显著下降?这其实也体现了这个问题的实际损失函数的形状,在这一区域上梯度比较平缓,以至于梯度下降算法并不能找到合适的突破方向寻找最优解,而是在原地徘徊。这一平缓地区就是损失函数的鞍点。
|
||||
|
||||
### 12.3.2 初始学习率的选择
|
||||
|
||||
我们前面一直使用固定的学习率,比如0.1或者0.05,而没有采用0.5、0.8这样高的学习率。这是因为在接近极小点时,损失函数的梯度也会变小,使用小的学习率时,不会担心步子太大越过极小点。
|
||||
|
||||
保证SGD收敛的充分条件是:
|
||||
|
||||
$$\sum_{k=1}^\infty \eta_k = \infty \tag{2}$$
|
||||
|
||||
且:
|
||||
|
||||
$$\sum_{k=1}^\infty \eta^2_k < \infty \tag{3}$$
|
||||
|
||||
图12-5是不同的学习率的选择对训练结果的影响。
|
||||
|
||||
<img src="./img/12/learning_rate.png" ch="500" />
|
||||
|
||||
图12-5 学习率对训练的影响
|
||||
|
||||
- 黄色:学习率太大,loss值增高,网络发散
|
||||
- 红色:学习率可以使网络收敛,但值较大,开始时loss值下降很快,但到达极值点附近时,在最优解附近来回跳跃
|
||||
- 绿色:正确的学习率设置
|
||||
- 蓝色:学习率值太小,loss值下降速度慢,训练次数长,收敛慢
|
||||
|
||||
有一种方式可以帮助我们快速找到合适的初始学习率。
|
||||
|
||||
Leslie N. Smith 在2015年的一篇论文[Cyclical Learning Rates for Training Neural Networks](https://arxiv.org/abs/1506.01186)中的描述了一个非常棒的方法来找初始学习率。
|
||||
|
||||
这个方法在论文中是用来估计网络允许的最小学习率和最大学习率,我们也可以用来找我们的最优初始学习率,方法非常简单:
|
||||
|
||||
1. 首先我们设置一个非常小的初始学习率,比如`1e-5`;
|
||||
2. 然后在每个`batch`之后都更新网络,计算损失函数值,同时增加学习率;
|
||||
3. 最后我们可以描绘出学习率的变化曲线和loss的变化曲线,从中就能够发现最好的学习率。
|
||||
|
||||
表12-2就是随着迭代次数的增加,学习率不断增加的曲线,以及不同的学习率对应的loss的曲线(理想中的曲线)。
|
||||
|
||||
表12-2 试验最佳学习率
|
||||
|
||||
|随着迭代次数增加学习率|观察Loss值与学习率的关系|
|
||||
|---|---|
|
||||
|<img src="..\Images\12\lr-select-1.jpg">|<img src="..\Images\12\lr-select-2.jpg">|
|
||||
|
||||
从表12-2的右图可以看到,学习率在0.3左右表现最好,再大就有可能发散了。我们把这个方法用于到我们的代码中试一下是否有效。
|
||||
|
||||
首先,设计一个数据结构,做出表12-3。
|
||||
|
||||
表12-3 学习率与迭代次数试验设计
|
||||
|
||||
|学习率段|0.0001~0.0009|0.001~0.009|0.01~0.09|0.1~0.9|1.0~1.1|
|
||||
|----|----|----|----|---|---|
|
||||
|步长|0.0001|0.001|0.01|0.1|0.01|
|
||||
|迭代|10|10|10|10|10|
|
||||
|
||||
对于每个学习率段,在每个点上迭代10次,然后:
|
||||
|
||||
$$当前学习率+步长 \rightarrow 下一个学习率$$
|
||||
|
||||
以第一段为例,会在0.1迭代100次,在0.2上迭代100次,......,在0.9上迭代100次。步长和迭代次数可以分段设置,得到图12-6。
|
||||
|
||||
<img src="./img/12/LR_try_1.png" ch="500" />
|
||||
|
||||
图12-6 第一轮的学习率测试
|
||||
|
||||
横坐标用了`np.log10()`函数来显示对数值,所以横坐标与学习率的对应关系如表12-4所示。
|
||||
|
||||
表12-4 横坐标与学习率的对应关系
|
||||
|
||||
|横坐标|-1.0|-0.8|-0.6|-0.4|-0.2|0.0|
|
||||
|--|--|--|--|--|--|--|
|
||||
|学习率|0.1|0.16|0.25|0.4|0.62|1.0|
|
||||
|
||||
前面一大段都是在下降,说明学习率为0.1、0.16、0.25、0.4时都太小了,那我们就继续探查-0.4后的段,得到第二轮测试结果如图12-7。
|
||||
|
||||
<img src="./img/12/LR_try_2.png" ch="500" />
|
||||
|
||||
图12-7 第二轮的学习率测试
|
||||
|
||||
到-0.13时(对应学习率0.74)开始,损失值上升,所以合理的初始学习率应该是0.7左右,于是我们再次把范围缩小的0.6,0.7,0.8去做试验,得到第三轮测试结果,如图12-8。
|
||||
|
||||
<img src="./img/12/LR_try_3.png" ch="500" />
|
||||
|
||||
图12-8 第三轮的学习率测试
|
||||
|
||||
最后得到的最佳初始学习率是0.8左右。由于loss值是渐渐从下降变为上升的,前面有一个积累的过程,如果想避免由于前几轮迭代带来的影响,可以使用比0.8小一些的数值,比如0.75作为初始学习率。
|
||||
|
||||
### 12.3.3 学习率的后期修正
|
||||
|
||||
用12.1的MNIST的例子,固定批大小为128时,我们分别使用学习率为0.2,0.3,0.5,0.8来比较一下学习曲线。
|
||||
|
||||
<img src="./img/12/acc_bs_128.png" ch="500" />
|
||||
|
||||
图12-9 不同学习率对应的迭代次数与准确度值的
|
||||
|
||||
学习率为0.5时效果最好,虽然0.8的学习率开始时上升得很快,但是到了10个`epoch`时,0.5的曲线就超上来了,最后稳定在0.8的曲线之上。
|
||||
|
||||
这就给了我们一个提示:可以在开始时,把学习率设置大一些,让准确率快速上升,损失值快速下降;到了一定阶段后,可以换用小一些的学习率继续训练。用公式表示:
|
||||
|
||||
$$
|
||||
LR_{new}=LR_{current} * DecayRate^{GlobalStep/DecaySteps} \tag{4}
|
||||
$$
|
||||
|
||||
举例来说:
|
||||
|
||||
- 当前的LR = 0.1
|
||||
- DecayRate = 0.9
|
||||
- DecaySteps = 50
|
||||
|
||||
公式变为:
|
||||
|
||||
$$lr = 0.1 * 0.9^{GlobalSteps/50}$$
|
||||
|
||||
意思是初始学习率为0.1,每训练50轮计算一次新的$lr$,是当前的$0.9^n$倍,其中$n$是正整数,因为一般用$GlobalSteps/50$的结果取整,所以$n=1,2,3,\ldots$
|
||||
|
||||
<img src="./img/12/lr_decay.png" ch="500" />
|
||||
|
||||
图12-10 阶梯状学习率下降法
|
||||
|
||||
如果计算一下每50轮的衰减的具体数值,见表12-5。
|
||||
|
||||
表12-5 学习率衰减值计算
|
||||
|
||||
|迭代|0|50|100|150|200|250|300|...|
|
||||
|---|---|---|---|---|---|---|---|---|
|
||||
|学习率|0.1|0.09|0.081|0.073|0.065|0.059|0.053|...|
|
||||
|
||||
这样的话,在开始时可以快速收敛,到后来变得很谨慎,小心翼翼地向极值点逼近,避免由于步子过大而跳过去。
|
||||
|
||||
上面描述的算法叫做step算法,还有一些其他的算法如下。
|
||||
|
||||
<img src="./img/12/lr_policy.png" ch="500" />
|
||||
|
||||
图12-11 其他各种学习率下降算法
|
||||
|
||||
#### fixed
|
||||
|
||||
使用固定的学习率,比如全程都用0.1。要注意的是,这个值不能大,否则在后期接近极值点时不易收敛。
|
||||
|
||||
#### step
|
||||
|
||||
每迭代一个预订的次数后(比如500步),就调低一次学习率。离散型,简单实用。
|
||||
|
||||
#### multistep
|
||||
|
||||
预设几个迭代次数,到达后调低学习率。与step不同的是,这里的次数可以是不均匀的,比如3000、5500、8000。离散型,简单实用。
|
||||
|
||||
#### exp
|
||||
|
||||
连续的指数变化的学习率,公式为:
|
||||
|
||||
$$lr_{new}=lr_{base} * \gamma^{iteration} \tag{5}$$
|
||||
|
||||
由于一般的iteration都很大(训练需要很多次迭代),所以学习率衰减得很快。$\gamma$可以取值0.9、0.99等接近于1的数值,数值越大,学习率的衰减越慢。
|
||||
|
||||
#### inv
|
||||
|
||||
倒数型变化,公式为:
|
||||
|
||||
$$lr_{new}=lr_{base} * \frac{1}{( 1 + \gamma * iteration)^{p}} \tag{6}$$
|
||||
|
||||
$\gamma$控制下降速率,取值越大下降速率越快;$p$控制最小极限值,取值越大时最小值越小,可以用0.5来做缺省值。
|
||||
|
||||
#### poly
|
||||
|
||||
多项式衰减,公式为:
|
||||
|
||||
$$lr_{new}=lr_{base} * (1 - {iteration \over iteration_{max}})^p \tag{7}$$
|
||||
|
||||
$p=1$时,为线性下降;$p>1$时,下降趋势向上突起;$p<1$时,下降趋势向下凹陷。$p$可以设置为0.9。
|
||||
|
||||
### 12.3.4 学习率与批大小的关系
|
||||
|
||||
#### 试验结果
|
||||
|
||||
我们回到MNIST的例子中,继续做试验。当批大小为32时,还是0.5的学习率最好,如图12-12所示。
|
||||
|
||||
<img src="./img/12/acc_bs_32.png" ch="500" />
|
||||
|
||||
图12-12 批大小为32时的几种学习率的比较
|
||||
|
||||
难道0.5是一个全局最佳学习率吗?别着急,继续降低批大小到16时,再观察准确率曲线。由于批大小缩小了一倍,所以要完成相同的`epoch`时,图12-13中的迭代次数会是图12-12中的两倍。
|
||||
|
||||
<img src="./img/12/acc_bs_16.png" ch="500" />
|
||||
|
||||
图12-13 批大小为16时几种学习率的比较
|
||||
|
||||
这次有了明显变化,一下子变成了0.1的学习率最好,这说明当批大小小到一定数量级后,学习率要和批大小匹配,较大的学习率配和较大的批量,反之亦然。
|
||||
|
||||
#### 原因解释
|
||||
|
||||
我们从试验中得到了这个直观的认识:大的批数值应该对应大的学习率,否则收敛很慢;小的批数值应该对应小的学习率,否则会收敛不到最佳点。
|
||||
|
||||
一个极端的情况是,当批大小为1时,即单个样本,由于噪音的存在,我们不能确定这个样本所代表的梯度方向就是正确的方向,但是我们又不能忽略这个样本的作用,所以往往采用很小的学习率。这种情况很适合于online-learning的场景,即流式训练。
|
||||
|
||||
使用Mini-batch的好处是可以克服单样本的噪音,此时就可以使用稍微大一些的学习率,让收敛速度变快,而不会由于样本噪音问题而偏离方向。从偏差方差的角度理解,单样本的偏差概率较大,多样本的偏差概率较小,而由于I.I.D.(独立同分布)的假设存在,多样本的方差是不会有太大变化的,即16个样本的方差和32个样本的方差应该差不多,那它们产生的梯度的方差也应该相似。
|
||||
|
||||
通常当我们增加batch size为原来的N倍时,要保证经过同样的样本后更新的权重相等,按照线性缩放规则,学习率应该增加为原来的m倍。但是如果要保证权重的梯度方差不变,则学习率应该增加为原来的$\sqrt m$倍。
|
||||
|
||||
研究表明,衰减学习率可以通过增加batch size来实现类似的效果,这实际上从SGD的权重更新式子就可以看出来两者确实是等价的。对于一个固定的学习率,存在一个最优的batch size能够最大化测试精度,这个batch size和学习率以及训练集的大小正相关。对此实际上是有两个建议:
|
||||
|
||||
1. 如果增加了学习率,那么batch size最好也跟着增加,这样收敛更稳定。
|
||||
2. 尽量使用大的学习率,因为很多研究都表明更大的学习率有利于提高泛化能力。如果真的要衰减,可以尝试其他办法,比如增加batch size,学习率对模型的收敛影响真的很大,慎重调整。
|
||||
|
||||
#### 数值理解
|
||||
|
||||
如果上述一些文字不容易理解的话,我们用一个最简单的示例来试图说明一下学习率与批大小的正比关系。
|
||||
|
||||
<img src="./img/12/lr_bs.png" />
|
||||
|
||||
图12-14 学习率与批大小关系的数值理解
|
||||
|
||||
先看图12-14中的左图:假设有三个蓝色样本点,正确的拟合直线如绿色直线所示,但是目前的拟合结果是红色直线,其斜率为0.5。我们来计算一下它的损失函数值,假设虚线组成的网格的单位值为1。
|
||||
|
||||
$$loss = \frac{1}{2m} \sum_i^m (z-y)^2 = (1^2 + 0^2 + 1^2)/2/3=0.333$$
|
||||
|
||||
损失函数值可以理解为是反向传播的误差回传力度,也就是说此时需要回传0.333的力度,就可以让红色直线向绿色直线的位置靠近,即,让$W$值变小,斜率从0.5变为0。
|
||||
|
||||
注意,我们下面如果用一个不太准确的计算来说明学习率与样本的关系,这个计算并不是真实存在于神经网络的,只是个数值上的直观解释。
|
||||
|
||||
我们需要一个学习率$\eta_1$,令:
|
||||
|
||||
$$w = w - \eta_1 * 0.333 = 0.5 - \eta_1 * 0.333 = 0$$
|
||||
|
||||
则:
|
||||
|
||||
$$\eta_1 = 1.5 \tag{8}$$
|
||||
|
||||
再看图12-14的右图,样本点变成5个,多了两个橙色的样本点,相当于批大小从3变成了5,计算损失函数值:
|
||||
|
||||
$$loss = (1^2+0.5^2+0^2+0.5^2+1^2)/2/5=0.25$$
|
||||
|
||||
样本数量增加了,由于样本服从I.I.D.分布,因此新的橙色样本位于蓝色样本之间。也因此损失函数值没有增加,反而从三个样本点的0.333降低到了五个样本点的0.25,此时,如果想回传同样的误差力度,使得w的斜率变为0,则:
|
||||
|
||||
$$w = w - \eta_2 * 0.25 = 0.5 - \eta_2 * 0.25 = 0$$
|
||||
|
||||
则:
|
||||
|
||||
$$\eta_2 = 2 \tag{9}$$
|
||||
|
||||
比较公式8和公式9的结果,样本数量增加了,学习率需要相应地增加。
|
||||
|
||||
大的batch size可以减少迭代次数,从而减少训练时间;另一方面,大的batch size的梯度计算更稳定,曲线平滑。在一定范围内,增加batch size有助于收敛的稳定性,但是过大的batch size会使得模型的泛化能力下降,验证或测试的误差增加。
|
||||
|
||||
batch size的增加可以比较随意,比如从16到32、64、128等等,而学习率是有上限的,从公式2和3知道,学习率不能大于1.0,这一点就如同Sigmoid函数一样,输入值可以变化很大,但很大的输入值会得到接近于1的输出值。因此batch size和学习率的关系可以大致总结如下:
|
||||
|
||||
1. 增加batch size,需要增加学习率来适应,可以用线性缩放的规则,成比例放大
|
||||
2. 到一定程度,学习率的增加会缩小,变成batch size的$\sqrt m$倍
|
||||
3. 到了比较极端的程度,无论batch size再怎么增加,也不能增加学习率了
|
||||
|
|
|
@ -3,100 +3,13 @@
|
|||
|
||||
## 14.1 回归任务功能测试
|
||||
|
||||
在第九章中,我们用一个两层的神经网络,验证了万能近似定理。当时是用hard code方式写的,现在我们用迷你框架来搭建一下。
|
||||
以下为本小节目录,详情请参阅《智能之门》正版图书,高等教育出版社。
|
||||
|
||||
### 14.1.1 搭建模型
|
||||
|
||||
这个模型很简单,一个双层的神经网络,第一层后面接一个Sigmoid激活函数,第二层直接输出拟合数据,如图14-2所示。
|
||||
|
||||
<img src="./img/14/ch09_net.png" />
|
||||
|
||||
图14-2 完成拟合任务的抽象模型
|
||||
|
||||
```Python
|
||||
def model():
|
||||
dataReader = LoadData()
|
||||
num_input = 1
|
||||
num_hidden1 = 4
|
||||
num_output = 1
|
||||
|
||||
max_epoch = 10000
|
||||
batch_size = 10
|
||||
learning_rate = 0.5
|
||||
|
||||
params = HyperParameters_4_0(
|
||||
learning_rate, max_epoch, batch_size,
|
||||
net_type=NetType.Fitting,
|
||||
init_method=InitialMethod.Xavier,
|
||||
stopper=Stopper(StopCondition.StopLoss, 0.001))
|
||||
|
||||
net = NeuralNet_4_0(params, "Level1_CurveFittingNet")
|
||||
fc1 = FcLayer_1_0(num_input, num_hidden1, params)
|
||||
net.add_layer(fc1, "fc1")
|
||||
sigmoid1 = ActivationLayer(Sigmoid())
|
||||
net.add_layer(sigmoid1, "sigmoid1")
|
||||
fc2 = FcLayer_1_0(num_hidden1, num_output, params)
|
||||
net.add_layer(fc2, "fc2")
|
||||
|
||||
net.train(dataReader, checkpoint=100, need_test=True)
|
||||
|
||||
net.ShowLossHistory()
|
||||
ShowResult(net, dataReader)
|
||||
```
|
||||
|
||||
超参数说明:
|
||||
|
||||
1. 输入层1个神经元,因为只有一个`x`值
|
||||
2. 隐层4个神经元,对于此问题来说应该是足够了,因为特征很少
|
||||
3. 输出层1个神经元,因为是拟合任务
|
||||
4. 学习率=0.5
|
||||
5. 最大`epoch=10000`轮
|
||||
6. 批量样本数=10
|
||||
7. 拟合网络类型
|
||||
8. Xavier初始化
|
||||
9. 绝对损失停止条件=0.001
|
||||
|
||||
### 14.1.2 训练结果
|
||||
|
||||
<img src="./img/14/ch09_loss.png" />
|
||||
|
||||
图14-3 训练过程中损失函数值和准确率的变化
|
||||
|
||||
如图14-3所示,损失函数值在一段平缓期过后,开始陡降,这种现象在神经网络的训练中是常见的,最有可能的是当时处于一个梯度变化的平缓地带,算法在艰难地寻找下坡路,然后忽然就找到了。这种情况同时也带来一个弊端:我们会经常遇到缓坡,到底要不要还继续训练?是不是再坚持一会儿就能找到出路呢?抑或是模型能力不够,永远找不到出路呢?这个问题没有准确答案,只能靠试验和经验了。
|
||||
|
||||
<img src="./img/14/ch09_result.png" />
|
||||
|
||||
图14-4 拟合结果
|
||||
|
||||
图14-4左侧子图是拟合的情况,绿色点是测试集数据,红色点是神经网路的推理结果,可以看到除了最左侧开始的部分,其它部分都拟合的不错。注意,这里我们不是在讨论过拟合、欠拟合的问题,我们在这个章节的目的就是更好地拟合一条曲线。
|
||||
|
||||
图14-4右侧的子图是用下面的代码生成的:
|
||||
|
||||
```Python
|
||||
y_test_real = net.inference(dr.XTest)
|
||||
axes.scatter(y_test_real, y_test_real-dr.YTestRaw, marker='o')
|
||||
```
|
||||
|
||||
以测试集的真实值为横坐标,以真实值和预测值的差为纵坐标。最理想的情况是所有点都在y=0处排成一条横线。从图上看,真实值和预测值二者的差异明显,但是请注意横坐标和纵坐标的间距相差一个数量级,所以差距其实不大。
|
||||
|
||||
再看打印输出的最后部分:
|
||||
|
||||
```
|
||||
epoch=4999, total_iteration=449999
|
||||
loss_train=0.000920, accuracy_train=0.968329
|
||||
loss_valid=0.000839, accuracy_valid=0.962375
|
||||
time used: 28.002626419067383
|
||||
save parameters
|
||||
total weights abs sum= 45.27530164993504
|
||||
total weights = 8
|
||||
little weights = 0
|
||||
zero weights = 0
|
||||
testing...
|
||||
0.9817814550687021
|
||||
0.9817814550687021
|
||||
```
|
||||
|
||||
由于我们设置了`eps=0.001`,所以在5000多个`epoch`时便达到了要求,训练停止。最后用测试集得到的准确率为98.17%,已经非常不错了。如果训练更多的轮,可以得到更好的结果。
|
||||
|
||||
### 代码位置
|
||||
|
||||
|
|
|
@ -3,152 +3,14 @@
|
|||
|
||||
## 14.2 回归任务 - 房价预测
|
||||
|
||||
以下为本小节目录,详情请参阅《智能之门》正版图书,高等教育出版社。
|
||||
|
||||
### 14.2.1 下载数据
|
||||
|
||||
数据集来自:"https://www.kaggle.com/datasets/harlfoxem/housesalesprediction/download?datasetVersionNumber=1",此数据集是 King County 地区2014年五月至2015年五月的房屋销售信息,适合于训练回归模型。
|
||||
|
||||
下载后是一个 archive.zip 文件,解压到 "ch14-DnnBasic\ExtendedDataReader\data\" 目录下成为 kc_house_data.csv 文件。
|
||||
|
||||
然后运行 "ch14-DnnBasic\ExtendedDataReader\Level2_HouseDataProcessor.py",将会生成两个文件:
|
||||
|
||||
- ch14.house.train.npz,训练文件
|
||||
- ch14.house.test.npz,测试文件
|
||||
|
||||
#### 数据字段解读
|
||||
|
||||
- id:唯一id
|
||||
- date:售出日期
|
||||
- price:售出价格(标签值)
|
||||
- bedrooms:卧室数量
|
||||
- bathrooms:浴室数量
|
||||
- sqft_living:居住面积
|
||||
- sqft_lot:停车场面积
|
||||
- floors:楼层数
|
||||
- waterfront:泳池
|
||||
- view:有多少次看房记录
|
||||
- condition:房屋状况
|
||||
- grade:评级
|
||||
- sqft_above:地面上的面积
|
||||
- sqft_basement:地下室的面积
|
||||
- yr_built:建筑年份
|
||||
- yr_renovated:翻修年份
|
||||
- zipcode:邮政编码
|
||||
- lat:维度
|
||||
- long:经度
|
||||
- sqft_living15:2015年翻修后的居住面积
|
||||
- sqft_lot15:2015年翻修后的停车场面积
|
||||
|
||||
一些考虑:
|
||||
|
||||
- 唯一id在数据库中有用,在训练时并不是一个特征,所以要去掉
|
||||
- 售出日期,由于是在一年内的数据,所以也没有用
|
||||
- sqft_liging15的值,如果非0的话,应该替换掉sqft_living
|
||||
- sqft_lot15的值,如果非0的话,应该替换掉sqft_lot
|
||||
- 邮政编码对应的地理位置过于宽泛,只能引起噪音,应该去掉
|
||||
- 返修年份,笔者认为它如果是非0值的话,可以替换掉建筑年份
|
||||
- 看房记录次数多并不能代表该房子价格就高,而是因为地理位置、价格、配置等满足特定人群的要求,所以笔者认为它不是必须的特征值
|
||||
|
||||
所以最后只留下13个字段。
|
||||
|
||||
#### 数据处理
|
||||
|
||||
原始数据只有一个数据集,所以需要我们自己把它分成训练集和测试集,比例大概为4:1。此数据集为`csv`文件格式,为了方便,我们把它转换成了两个扩展名为`npz`的`numpy`压缩形式:
|
||||
|
||||
- `house_Train.npz`,训练数据集
|
||||
- `house_Test.npz`,测试数据集
|
||||
|
||||
#### 加载数据
|
||||
|
||||
与上面第一个例子的代码相似,但是房屋数据属性繁杂,所以需要做归一化,房屋价格也是至少6位数,所以也需要做归一化。
|
||||
|
||||
这里有个需要注意的地方,即训练集和测试集的数据,需要合并在一起做归一化,然后再分开使用。为什么要先合并呢?假设训练集样本中的房屋面积的范围为150到220,而测试集中的房屋面积有可能是160到230,两者不一致。分别归一化的话,150变成0,160也变成0,这样预测就会产生误差。
|
||||
|
||||
最后还需要在训练集中用`GenerateValidaionSet(k=10)`分出一个1:9的验证集。
|
||||
|
||||
### 14.2.2 搭建模型
|
||||
|
||||
在不知道一个问题的实际复杂度之前,我们不妨把模型设计得复杂一些。如下图所示,这个模型包含了四组全连接层-Relu层的组合,最后是一个单输出做拟合。
|
||||
|
||||
<img src="./img/14/non_linear_regression.png" />
|
||||
|
||||
图14-5 完成房价预测任务的抽象模型
|
||||
|
||||
```Python
|
||||
def model():
|
||||
dr = LoadData()
|
||||
|
||||
num_input = dr.num_feature
|
||||
num_hidden1 = 32
|
||||
num_hidden2 = 16
|
||||
num_hidden3 = 8
|
||||
num_hidden4 = 4
|
||||
num_output = 1
|
||||
|
||||
max_epoch = 1000
|
||||
batch_size = 16
|
||||
learning_rate = 0.1
|
||||
|
||||
params = HyperParameters_4_0(
|
||||
learning_rate, max_epoch, batch_size,
|
||||
net_type=NetType.Fitting,
|
||||
init_method=InitialMethod.Xavier,
|
||||
stopper=Stopper(StopCondition.StopDiff, 1e-7))
|
||||
|
||||
net = NeuralNet_4_0(params, "HouseSingle")
|
||||
|
||||
fc1 = FcLayer_1_0(num_input, num_hidden1, params)
|
||||
net.add_layer(fc1, "fc1")
|
||||
r1 = ActivationLayer(Relu())
|
||||
net.add_layer(r1, "r1")
|
||||
......
|
||||
fc5 = FcLayer_1_0(num_hidden4, num_output, params)
|
||||
net.add_layer(fc5, "fc5")
|
||||
|
||||
net.train(dr, checkpoint=10, need_test=True)
|
||||
|
||||
output = net.inference(dr.XTest)
|
||||
real_output = dr.DeNormalizeY(output)
|
||||
mse = np.sum((dr.YTestRaw - real_output)**2)/dr.YTest.shape[0]/10000
|
||||
print("mse=", mse)
|
||||
|
||||
net.ShowLossHistory()
|
||||
|
||||
ShowResult(net, dr)
|
||||
```
|
||||
|
||||
超参数说明:
|
||||
|
||||
1. 学习率=0.1
|
||||
2. 最大`epoch=1000`
|
||||
3. 批大小=16
|
||||
4. 拟合网络
|
||||
5. 初始化方法Xavier
|
||||
6. 停止条件为相对误差`1e-7`
|
||||
|
||||
net.train()函数是一个阻塞函数,只有当训练完毕后才返回。
|
||||
|
||||
在train后面的部分,是用测试集来测试该模型的准确度,使用了数据城堡(Data Castle)的官方评测方法,用均方差除以10000,得到的数字越小越好。一般的模型大概是一个7位数的结果,稍微好一些的是6位数。
|
||||
|
||||
### 14.2.3 训练结果
|
||||
|
||||
<img src="./img/14/house_loss.png" />
|
||||
|
||||
图14-6 训练过程中损失函数值和准确率的变化
|
||||
|
||||
由于标签数据也做了归一化,变换为都是0至1间的小数,所以均方差的数值很小,需要观察小数点以后的第4位。从图14-6中可以看到,损失函数值很快就降到了0.0002以下,然后就很缓慢地下降。而精度值在不断的上升,相信更多的迭代次数会带来更高的精度。
|
||||
|
||||
再看下面的打印输出部分,用R2_Score法得到的值为0.841,而用数据城堡官方的评测标准,得到的MSE值为2384411,还比较大,说明模型精度还应该有上升的空间。
|
||||
|
||||
```
|
||||
......
|
||||
epoch=999, total_iteration=972999
|
||||
loss_train=0.000079, accuracy_train=0.740406
|
||||
loss_valid=0.000193, accuracy_valid=0.857289
|
||||
time used: 193.5549156665802
|
||||
testing...
|
||||
0.8412989144927305
|
||||
mse= 2384411.5840510926
|
||||
```
|
||||
|
||||
### 代码位置
|
||||
|
||||
|
|
|
@ -3,80 +3,13 @@
|
|||
|
||||
## 14.3 二分类任务功能测试
|
||||
|
||||
以下为本小节目录,详情请参阅《智能之门》正版图书,高等教育出版社。
|
||||
|
||||
### 14.3.1 搭建模型
|
||||
|
||||
同样是一个双层神经网络,但是最后一层要接一个Logistic二分类函数来完成二分类任务,如图14-7所示。
|
||||
|
||||
<img src="./img/14/ch10_net.png" />
|
||||
|
||||
图14-7 完成非线性二分类教学案例的抽象模型
|
||||
|
||||
```Python
|
||||
|
||||
def model(dataReader):
|
||||
num_input = 2
|
||||
num_hidden = 3
|
||||
num_output = 1
|
||||
|
||||
max_epoch = 1000
|
||||
batch_size = 5
|
||||
learning_rate = 0.1
|
||||
|
||||
params = HyperParameters_4_0(
|
||||
learning_rate, max_epoch, batch_size,
|
||||
net_type=NetType.BinaryClassifier,
|
||||
init_method=InitialMethod.Xavier,
|
||||
stopper=Stopper(StopCondition.StopLoss, 0.02))
|
||||
|
||||
net = NeuralNet_4_0(params, "Arc")
|
||||
|
||||
fc1 = FcLayer_1_0(num_input, num_hidden, params)
|
||||
net.add_layer(fc1, "fc1")
|
||||
sigmoid1 = ActivationLayer(Sigmoid())
|
||||
net.add_layer(sigmoid1, "sigmoid1")
|
||||
|
||||
fc2 = FcLayer_1_0(num_hidden, num_output, params)
|
||||
net.add_layer(fc2, "fc2")
|
||||
logistic = ClassificationLayer(Logistic())
|
||||
net.add_layer(logistic, "logistic")
|
||||
|
||||
net.train(dataReader, checkpoint=10, need_test=True)
|
||||
return net
|
||||
```
|
||||
|
||||
超参数说明:
|
||||
|
||||
1. 输入层神经元数为2
|
||||
2. 隐层的神经元数为3,使用Sigmoid激活函数
|
||||
3. 由于是二分类任务,所以输出层只有一个神经元,用Logistic做二分类函数
|
||||
4. 最多训练1000轮
|
||||
5. 批大小=5
|
||||
6. 学习率=0.1
|
||||
7. 绝对误差停止条件=0.02
|
||||
|
||||
### 14.3.2 运行结果
|
||||
|
||||
<img src="./img/14/ch10_loss.png" />
|
||||
|
||||
图14-8 训练过程中损失函数值和准确率的变化
|
||||
|
||||
图14-8是训练记录,再看下面的打印输出结果:
|
||||
|
||||
```
|
||||
......
|
||||
epoch=419, total_iteration=30239
|
||||
loss_train=0.010094, accuracy_train=1.000000
|
||||
loss_valid=0.019141, accuracy_valid=1.000000
|
||||
time used: 2.149379253387451
|
||||
testing...
|
||||
1.0
|
||||
```
|
||||
最后的testing...的结果是1.0,表示100%正确,这初步说明mini框架在这个基本case上工作得很好。图14-9所示的分类效果也不错。
|
||||
|
||||
<img src="./img/14/ch10_result.png" ch="500" />
|
||||
|
||||
图14-9 分类效果
|
||||
|
||||
### 代码位置
|
||||
|
||||
ch14, Level3
|
||||
|
|
|
@ -3,153 +3,15 @@
|
|||
|
||||
## 14.4 二分类任务真实案例
|
||||
|
||||
我们用一个真实的数据级来实现一个二分类任务:收入调查与预测,即给定一个居民的各种情况,如工作、家庭、学历等,来预测该居民的年收入是否可以大于50K/年,所以大于50K的就是正例,而小于等于50K的就是负例。
|
||||
以下为本小节目录,详情请参阅《智能之门》正版图书,高等教育出版社。
|
||||
|
||||
### 14.4.1 准备数据
|
||||
|
||||
此数据集是从1994 Census数据库中提取的$^{[1]}$,具体路径是 "https://archive.ics.uci.edu/ml/machine-learning-databases/adult/",下载其中的 adult.data 和 adult.test 两个文件拷贝到 "ch14-DnnBasic\ExtendedDataReader\data\" 目录下。
|
||||
|
||||
然后运行 "ch14-DnnBasic\ExtendedDataReader\Level4_IncomeDataProcessor.py",将会在 "data" 目录下得到两个文件:
|
||||
|
||||
- ch14.Income.train.npz,训练数据
|
||||
- ch14.Income.test.npz, 测试数据
|
||||
|
||||
#### 数据字段解读
|
||||
|
||||
标签值:>50K,<=50K。
|
||||
|
||||
属性字段:
|
||||
|
||||
- `age`,年龄:连续值
|
||||
- `workclass`,工作性质:枚举型,类似私企、政府之类的
|
||||
- `fnlwgt`,权重:连续值
|
||||
- `education`,教育程度:枚举型,如学士、硕士等
|
||||
- `education-num`,受教育的时长:连续值
|
||||
- `marital-status`,婚姻状况:枚举型,已婚、未婚、离异等
|
||||
- `occupation`,职业:枚举型,包含的种类很多,如技术支持、维修工、销售、农民渔民、军人等
|
||||
- `relationship`,家庭角色:枚举型,丈夫、妻子等
|
||||
- `sex`,性别:枚举型
|
||||
- `capital-gain`,资本收益:连续值
|
||||
- `capitial-loss`,资本损失:连续值
|
||||
- `hours-per-week`,每周工作时长:连续值
|
||||
- `native-country`,祖籍:枚举型
|
||||
|
||||
#### 数据处理
|
||||
|
||||
数据分析和数据处理实际上是一门独立的课,超出类本书的范围,所以我们只做一些简单的数据处理,以便神经网络可以用之训练。
|
||||
|
||||
对于连续值,我们可以直接使用原始数据。对于枚举型,我们需要把它们转成连续值。以性别举例,`Female=0`,`Male=1`即可。对于其它枚举型,都可以用从0开始的整数编码。
|
||||
|
||||
一个小技巧是利用`python`的`list`功能,取元素下标,即可以作为整数编码:
|
||||
|
||||
```Python
|
||||
sex_list = ["Female", "Male"]
|
||||
array_x[0,9] = sex_list.index(row[9].strip())
|
||||
```
|
||||
|
||||
`strip()`是trim掉前面的空格,因为是`csv`格式,读出来会是这个样子:"_Female",前面总有个空格。`index`是取列表下标,这样对于字符串"Female"取出的下标为0,对于字符串"Male"取出的下标为1。
|
||||
|
||||
把所有数据按行保存到`numpy`数组中,最后用`npz`格式存储:
|
||||
```Python
|
||||
np.savez(data_npz, data=self.XData, label=self.YData)
|
||||
```
|
||||
原始数据已经把train data和test data分开了,所以我们针对两个数据集分别调用数据处理过程一次,保存为`Income_Train.npz`和`Income_Test.npz`。
|
||||
|
||||
#### 加载数据
|
||||
|
||||
```Python
|
||||
train_file = "../../Data/ch14.Income.train.npz"
|
||||
test_file = "../../Data/ch14.Income.test.npz"
|
||||
|
||||
def LoadData():
|
||||
dr = DataReader_2_0(train_file, test_file)
|
||||
dr.ReadData()
|
||||
dr.NormalizeX()
|
||||
dr.Shuffle()
|
||||
dr.GenerateValidationSet()
|
||||
return dr
|
||||
```
|
||||
|
||||
因为属性字段众多,取值范围相差很大,所以一定要先调用`NormalizeX()`函数做归一化。由于是二分类问题,在做数据处理时,我们已经把大于50K标记为1,小于等于50K标记为0,所以不需要做标签值的归一化。
|
||||
|
||||
### 14.4.2 搭建模型
|
||||
|
||||
我们搭建一个与14.2一样的网络结构,不同的是为了完成二分类任务,在最后接一个Logistic函数。
|
||||
|
||||
<img src="./img/14/income_net.png" />
|
||||
|
||||
图14-10 完成二分类真实案例的抽象模型
|
||||
|
||||
```Python
|
||||
def model(dr):
|
||||
num_input = dr.num_feature
|
||||
num_hidden1 = 32
|
||||
num_hidden2 = 16
|
||||
num_hidden3 = 8
|
||||
num_hidden4 = 4
|
||||
num_output = 1
|
||||
|
||||
max_epoch = 100
|
||||
batch_size = 16
|
||||
learning_rate = 0.1
|
||||
|
||||
params = HyperParameters_4_0(
|
||||
learning_rate, max_epoch, batch_size,
|
||||
net_type=NetType.BinaryClassifier,
|
||||
init_method=InitialMethod.MSRA,
|
||||
stopper=Stopper(StopCondition.StopDiff, 1e-3))
|
||||
|
||||
net = NeuralNet_4_0(params, "Income")
|
||||
|
||||
fc1 = FcLayer_1_0(num_input, num_hidden1, params)
|
||||
net.add_layer(fc1, "fc1")
|
||||
a1 = ActivationLayer(Relu())
|
||||
net.add_layer(a1, "relu1")
|
||||
......
|
||||
fc5 = FcLayer_1_0(num_hidden4, num_output, params)
|
||||
net.add_layer(fc5, "fc5")
|
||||
logistic = ClassificationLayer(Logistic())
|
||||
net.add_layer(logistic, "logistic")
|
||||
|
||||
net.train(dr, checkpoint=1, need_test=True)
|
||||
return net
|
||||
```
|
||||
|
||||
超参数说明:
|
||||
|
||||
1. 学习率=0.1
|
||||
2. 最大`epoch=100`
|
||||
3. 批大小=16
|
||||
4. 二分类网络类型
|
||||
5. MSRA初始化
|
||||
6. 相对误差停止条件1e-3
|
||||
|
||||
`net.train()`函数是一个阻塞函数,只有当训练完毕后才返回。
|
||||
|
||||
### 14.4.3 训练结果
|
||||
|
||||
下图左边是损失函数图,右边是准确率图。忽略测试数据的波动,只看红色的验证集的趋势,损失函数值不断下降,准确率不断上升。
|
||||
|
||||
为什么不把`max_epoch`设置为更大的数字,比如1000,以便得到更好的结果呢?实际上,训练更多的次数,因为过拟合的风险,不会得到更好的结果。有兴趣的读者可以自己试验一下。
|
||||
|
||||
<img src="./img/14/income_loss.png" />
|
||||
|
||||
图14-11 训练过程中损失函数值和准确率的变化
|
||||
|
||||
下面是最后的打印输出:
|
||||
|
||||
```
|
||||
......
|
||||
epoch=99, total_iteration=169699
|
||||
loss_train=0.296219, accuracy_train=0.800000
|
||||
loss_valid=nan, accuracy_valid=0.838859
|
||||
time used: 29.866002321243286
|
||||
testing...
|
||||
0.8431606905710491
|
||||
```
|
||||
|
||||
最后用独立的测试集得到的结果是84%,与该数据集相关的其它论文相比,已经是一个不错的成绩了。
|
||||
|
||||
### 代码位置
|
||||
|
||||
ch14, Level4
|
||||
|
|
|
@ -3,140 +3,14 @@
|
|||
|
||||
## 14.5 多分类功能测试
|
||||
|
||||
在第11章里,我们讲解了如何使用神经网络做多分类。在本节我们将会用Mini框架重现那个教学案例,然后使用一个真实的案例验证多分类的用法。
|
||||
以下为本小节目录,详情请参阅《智能之门》正版图书,高等教育出版社。
|
||||
|
||||
### 14.5.1 搭建模型一
|
||||
|
||||
#### 模型
|
||||
|
||||
使用Sigmoid做为激活函数的两层网络,如图14-12。
|
||||
|
||||
<img src="./img/14/ch11_net_sigmoid.png" />
|
||||
|
||||
图14-12 完成非线性多分类教学案例的抽象模型
|
||||
|
||||
#### 代码
|
||||
|
||||
```Python
|
||||
def model_sigmoid(num_input, num_hidden, num_output, hp):
|
||||
net = NeuralNet_4_0(hp, "chinabank_sigmoid")
|
||||
|
||||
fc1 = FcLayer_1_0(num_input, num_hidden, hp)
|
||||
net.add_layer(fc1, "fc1")
|
||||
s1 = ActivationLayer(Sigmoid())
|
||||
net.add_layer(s1, "Sigmoid1")
|
||||
|
||||
fc2 = FcLayer_1_0(num_hidden, num_output, hp)
|
||||
net.add_layer(fc2, "fc2")
|
||||
softmax1 = ClassificationLayer(Softmax())
|
||||
net.add_layer(softmax1, "softmax1")
|
||||
|
||||
net.train(dataReader, checkpoint=50, need_test=True)
|
||||
net.ShowLossHistory()
|
||||
|
||||
ShowResult(net, hp.toString())
|
||||
ShowData(dataReader)
|
||||
```
|
||||
|
||||
#### 超参数说明
|
||||
|
||||
1. 隐层8个神经元
|
||||
2. 最大`epoch=5000`
|
||||
3. 批大小=10
|
||||
4. 学习率0.1
|
||||
5. 绝对误差停止条件=0.08
|
||||
6. 多分类网络类型
|
||||
7. 初始化方法为Xavier
|
||||
|
||||
`net.train()`函数是一个阻塞函数,只有当训练完毕后才返回。
|
||||
|
||||
#### 运行结果
|
||||
|
||||
训练过程如图14-13所示,分类效果如图14-14所示。
|
||||
|
||||
<img src="./img/14/ch11_loss_sigmoid.png" />
|
||||
|
||||
图14-13 训练过程中损失函数值和准确率的变化
|
||||
|
||||
<img src="./img/14/ch11_result_sigmoid.png" ch="500" />
|
||||
|
||||
图14-14 分类效果图
|
||||
|
||||
### 14.5.2 搭建模型二
|
||||
|
||||
#### 模型
|
||||
|
||||
使用ReLU做为激活函数的三层网络,如图14-15。
|
||||
|
||||
<img src="./img/14/ch11_net_relu.png" />
|
||||
|
||||
图14-15 使用ReLU函数抽象模型
|
||||
|
||||
用两层网络也可以实现,但是使用ReLE函数时,训练效果不是很稳定,用三层比较保险。
|
||||
|
||||
#### 代码
|
||||
|
||||
```Python
|
||||
def model_relu(num_input, num_hidden, num_output, hp):
|
||||
net = NeuralNet_4_0(hp, "chinabank_relu")
|
||||
|
||||
fc1 = FcLayer_1_0(num_input, num_hidden, hp)
|
||||
net.add_layer(fc1, "fc1")
|
||||
r1 = ActivationLayer(Relu())
|
||||
net.add_layer(r1, "Relu1")
|
||||
|
||||
fc2 = FcLayer_1_0(num_hidden, num_hidden, hp)
|
||||
net.add_layer(fc2, "fc2")
|
||||
r2 = ActivationLayer(Relu())
|
||||
net.add_layer(r2, "Relu2")
|
||||
|
||||
fc3 = FcLayer_1_0(num_hidden, num_output, hp)
|
||||
net.add_layer(fc3, "fc3")
|
||||
softmax = ClassificationLayer(Softmax())
|
||||
net.add_layer(softmax, "softmax")
|
||||
|
||||
net.train(dataReader, checkpoint=50, need_test=True)
|
||||
net.ShowLossHistory()
|
||||
|
||||
ShowResult(net, hp.toString())
|
||||
ShowData(dataReader)
|
||||
```
|
||||
|
||||
#### 超参数说明
|
||||
|
||||
1. 隐层8个神经元
|
||||
2. 最大`epoch=5000`
|
||||
3. 批大小=10
|
||||
4. 学习率0.1
|
||||
5. 绝对误差停止条件=0.08
|
||||
6. 多分类网络类型
|
||||
7. 初始化方法为MSRA
|
||||
|
||||
#### 运行结果
|
||||
|
||||
训练过程如图14-16所示,分类效果如图14-17所示。
|
||||
|
||||
<img src="./img/14/ch11_loss_relu.png" />
|
||||
|
||||
图14-16 训练过程中损失函数值和准确率的变化
|
||||
|
||||
<img src="./img/14/ch11_result_relu.png" ch="500" />
|
||||
|
||||
图14-17 分类效果图
|
||||
|
||||
### 14.5.3 比较
|
||||
|
||||
表14-1比较一下使用不同的激活函数的分类效果图。
|
||||
|
||||
表14-1 使用不同的激活函数的分类结果比较
|
||||
|
||||
|Sigmoid|ReLU|
|
||||
|---|---|
|
||||
|<img src='./img/14/ch11_result_sigmoid.png'/>|<img src='./img/14/ch11_result_relu.png'/>|
|
||||
|
||||
可以看到左图中的边界要平滑许多,这也就是ReLU和Sigmoid的区别,ReLU是用分段线性拟合曲线,Sigmoid有真正的曲线拟合能力。但是Sigmoid也有缺点,看分类的边界,使用ReLU函数的分类边界比较清晰,而使用Sigmoid函数的分类边界要平缓一些,过渡区较宽。
|
||||
|
||||
用一句简单的话来描述二者的差别:Relu能直则直,对方形边界适用;Sigmoid能弯则弯,对圆形边界适用。
|
||||
|
||||
### 代码位置
|
||||
|
||||
|
|
|
@ -3,88 +3,16 @@
|
|||
|
||||
## 14.6 多分类任务 - MNIST手写体识别
|
||||
|
||||
以下为本小节目录,详情请参阅《智能之门》正版图书,高等教育出版社。
|
||||
|
||||
### 14.6.1 数据读取
|
||||
|
||||
把 "ch12-MultipleLayerNetwoek\HelperClass2\data\" 目录下的四个 MNIST 相关的文件拷贝到 "ch14-DnnBasic\ExtendedDataReader\data\" 目录下。
|
||||
|
||||
MNIST数据本身是图像格式的,我们用`mode="vector"`去读取,转变成矢量格式。
|
||||
|
||||
```Python
|
||||
def LoadData():
|
||||
print("reading data...")
|
||||
dr = MnistImageDataReader(mode="vector")
|
||||
......
|
||||
```
|
||||
|
||||
### 14.6.2 搭建模型
|
||||
|
||||
一共4个隐层,都用ReLU激活函数连接,最后的输出层接Softmax分类函数。
|
||||
|
||||
<img src="./img/14/mnist_net.png" />
|
||||
|
||||
图14-18 完成MNIST分类任务的抽象模型
|
||||
|
||||
以下是主要的参数设置:
|
||||
|
||||
```Python
|
||||
if __name__ == '__main__':
|
||||
dataReader = LoadData()
|
||||
num_feature = dataReader.num_feature
|
||||
num_example = dataReader.num_example
|
||||
num_input = num_feature
|
||||
num_hidden1 = 128
|
||||
num_hidden2 = 64
|
||||
num_hidden3 = 32
|
||||
num_hidden4 = 16
|
||||
num_output = 10
|
||||
max_epoch = 10
|
||||
batch_size = 64
|
||||
learning_rate = 0.1
|
||||
|
||||
params = HyperParameters_4_0(
|
||||
learning_rate, max_epoch, batch_size,
|
||||
net_type=NetType.MultipleClassifier,
|
||||
init_method=InitialMethod.MSRA,
|
||||
stopper=Stopper(StopCondition.StopLoss, 0.12))
|
||||
|
||||
net = NeuralNet_4_0(params, "MNIST")
|
||||
|
||||
fc1 = FcLayer_1_0(num_input, num_hidden1, params)
|
||||
net.add_layer(fc1, "fc1")
|
||||
r1 = ActivationLayer(Relu())
|
||||
net.add_layer(r1, "r1")
|
||||
......
|
||||
fc5 = FcLayer_1_0(num_hidden4, num_output, params)
|
||||
net.add_layer(fc5, "fc5")
|
||||
softmax = ClassificationLayer(Softmax())
|
||||
net.add_layer(softmax, "softmax")
|
||||
|
||||
net.train(dataReader, checkpoint=0.05, need_test=True)
|
||||
net.ShowLossHistory(xcoord=XCoordinate.Iteration)
|
||||
```
|
||||
|
||||
### 14.6.3 运行结果
|
||||
|
||||
我们设计的停止条件是绝对Loss值达到0.12时,所以迭代到6个epoch时,达到了0.119的损失值,就停止训练了。
|
||||
|
||||
<img src="./img/14/mnist_loss.png" />
|
||||
|
||||
图14-19 训练过程中损失函数值和准确率的变化
|
||||
|
||||
图14-19是训练过程图示,下面是最后几行的打印输出。
|
||||
|
||||
```
|
||||
......
|
||||
epoch=6, total_iteration=5763
|
||||
loss_train=0.005559, accuracy_train=1.000000
|
||||
loss_valid=0.119701, accuracy_valid=0.971667
|
||||
time used: 17.500738859176636
|
||||
save parameters
|
||||
testing...
|
||||
0.9697
|
||||
```
|
||||
|
||||
最后用测试集得到的准确率为96.97%。
|
||||
|
||||
### 代码位置
|
||||
|
||||
|
|
|
@ -3,172 +3,19 @@
|
|||
|
||||
## 15.1 权重矩阵初始化
|
||||
|
||||
权重矩阵初始化是一个非常重要的环节,是训练神经网络的第一步,选择正确的初始化方法会带了事半功倍的效果。这就好比攀登喜马拉雅山,如果选择从南坡登山,会比从北坡容易很多。而初始化权重矩阵,相当于下山时选择不同的道路,在选择之前并不知道这条路的难易程度,只是知道它可以抵达山下。这种选择是随机的,即使你使用了正确的初始化算法,每次重新初始化时也会给训练结果带来很多影响。
|
||||
|
||||
比如第一次初始化时得到权重值为(0.12847,0.36453),而第二次初始化得到(0.23334,0.24352),经过试验,第一次初始化用了3000次迭代达到精度为96%的模型,第二次初始化只用了2000次迭代就达到了相同精度。这种情况在实践中是常见的。
|
||||
以下为本小节目录,详情请参阅《智能之门》正版图书,高等教育出版社。
|
||||
|
||||
### 15.1.1 零初始化
|
||||
|
||||
即把所有层的`W`值的初始值都设置为0。
|
||||
|
||||
$$
|
||||
W = 0
|
||||
$$
|
||||
|
||||
但是对于多层网络来说,绝对不能用零初始化,否则权重值不能学习到合理的结果。看下面的零值初始化的权重矩阵值打印输出:
|
||||
```
|
||||
W1= [[-0.82452497 -0.82452497 -0.82452497]]
|
||||
B1= [[-0.01143752 -0.01143752 -0.01143752]]
|
||||
W2= [[-0.68583865]
|
||||
[-0.68583865]
|
||||
[-0.68583865]]
|
||||
B2= [[0.68359678]]
|
||||
```
|
||||
|
||||
可以看到`W1`、`B1`、`W2`内部3个单元的值都一样,这是因为初始值都是0,所以梯度均匀回传,导致所有`W`的值都同步更新,没有差别。这样的话,无论多少轮,最终的结果也不会正确。
|
||||
|
||||
### 15.1.2 标准初始化
|
||||
|
||||
标准正态初始化方法保证激活函数的输入均值为0,方差为1。将W按如下公式进行初始化:
|
||||
|
||||
$$
|
||||
W \sim N \begin{bmatrix} 0, 1 \end{bmatrix}
|
||||
$$
|
||||
|
||||
其中的W为权重矩阵,N表示高斯分布,Gaussian Distribution,也叫做正态分布,Normal Distribution,所以有的地方也称这种初始化为Normal初始化。
|
||||
|
||||
一般会根据全连接层的输入和输出数量来决定初始化的细节:
|
||||
|
||||
$$
|
||||
W \sim N
|
||||
\begin{pmatrix}
|
||||
0, \frac{1}{\sqrt{n_{in}}}
|
||||
\end{pmatrix}
|
||||
$$
|
||||
|
||||
$$
|
||||
W \sim U
|
||||
\begin{pmatrix}
|
||||
-\frac{1}{\sqrt{n_{in}}}, \frac{1}{\sqrt{n_{in}}}
|
||||
\end{pmatrix}
|
||||
$$
|
||||
|
||||
当目标问题较为简单时,网络深度不大,所以用标准初始化就可以了。但是当使用深度网络时,会遇到如图15-1所示的问题。
|
||||
|
||||
<img src="./img/15/init_normal_sigmoid.png" ch="500" />
|
||||
|
||||
图15-1 标准初始化在Sigmoid激活函数上的表现
|
||||
|
||||
图15-1是一个6层的深度网络,使用全连接层+Sigmoid激活函数,图中表示的是各层激活函数的直方图。可以看到各层的激活值严重向两侧[0,1]靠近,从Sigmoid的函数曲线可以知道这些值的导数趋近于0,反向传播时的梯度逐步消失。处于中间地段的值比较少,对参数学习非常不利。
|
||||
|
||||
### 15.1.3 Xavier初始化方法
|
||||
|
||||
基于上述观察,Xavier Glorot等人研究出了下面的Xavier$^{[1]}$初始化方法。
|
||||
|
||||
条件:正向传播时,激活值的方差保持不变;反向传播时,关于状态值的梯度的方差保持不变。
|
||||
|
||||
$$
|
||||
W \sim N
|
||||
\begin{pmatrix}
|
||||
0, \sqrt{\frac{2}{n_{in} + n_{out}}}
|
||||
\end{pmatrix}
|
||||
$$
|
||||
|
||||
$$
|
||||
W \sim U
|
||||
\begin{pmatrix}
|
||||
-\sqrt{\frac{6}{n_{in} + n_{out}}}, \sqrt{\frac{6}{n_{in} + n_{out}}}
|
||||
\end{pmatrix}
|
||||
$$
|
||||
|
||||
其中的W为权重矩阵,N表示正态分布(Normal Distribution),U表示均匀分布(Uniform Distribution)。下同。
|
||||
|
||||
假设激活函数关于0对称,且主要针对于全连接神经网络。适用于tanh和softsign。
|
||||
|
||||
即权重矩阵参数应该满足在该区间内的均匀分布。其中的W是权重矩阵,U是Uniform分布,即均匀分布。
|
||||
|
||||
论文摘要:神经网络在2006年之前不能很理想地工作,很大原因在于权重矩阵初始化方法上。Sigmoid函数不太适合于深度学习,因为会导致梯度饱和。基于以上原因,我们提出了一种可以快速收敛的参数初始化方法。
|
||||
|
||||
Xavier初始化方法比直接用高斯分布进行初始化W的优势所在:
|
||||
|
||||
一般的神经网络在前向传播时神经元输出值的方差会不断增大,而使用Xavier等方法理论上可以保证每层神经元输入输出方差一致。
|
||||
|
||||
图15-2是深度为6层的网络中的表现情况,可以看到,后面几层的激活函数输出值的分布仍然基本符合正态分布,利于神经网络的学习。
|
||||
|
||||
<img src="./img/15/init_xavier_sigmoid.png" ch="500" />
|
||||
|
||||
图15-2 Xavier初始化在Sigmoid激活函数上的表现
|
||||
|
||||
表15-1 随机初始化和Xavier初始化的各层激活值与反向传播梯度比较
|
||||
|
||||
| |各层的激活值|各层的反向传播梯度|
|
||||
|---|---|---|
|
||||
| 随机初始化 |<img src=".\img\15\forward_activation1.png"><br/>激活值分布渐渐集中|<img src=".\img\15\backward_activation1.png"><br/>反向传播力度逐层衰退|
|
||||
| Xavier初始化 |<img src=".\img\15\forward_activation2.png"><br/>激活值分布均匀|<img src=".\img\15\backward_activation2.png"><br/>反向传播力度保持不变|
|
||||
|
||||
但是,随着深度学习的发展,人们觉得Sigmoid的反向力度受限,又发明了ReLU激活函数。图15-3显示了Xavier初始化在ReLU激活函数上的表现。
|
||||
|
||||
<img src="./img/15/init_xavier_relu.png" ch="500" />
|
||||
|
||||
图15-3 Xavier初始化在ReLU激活函数上的表现
|
||||
|
||||
可以看到,随着层的加深,使用ReLU时激活值逐步向0偏向,同样会导致梯度消失问题。于是He Kaiming等人研究出了MSRA初始化法,又叫做He初始化法。
|
||||
|
||||
### 15.1.4 MSRA初始化方法
|
||||
|
||||
MSRA初始化方法$^{[2]}$,又叫做He方法,因为作者姓何。
|
||||
|
||||
条件:正向传播时,状态值的方差保持不变;反向传播时,关于激活值的梯度的方差保持不变。
|
||||
|
||||
网络初始化是一件很重要的事情。但是,传统的固定方差的高斯分布初始化,在网络变深的时候使得模型很难收敛。VGG团队是这样处理初始化的问题的:他们首先训练了一个8层的网络,然后用这个网络再去初始化更深的网络。
|
||||
|
||||
“Xavier”是一种相对不错的初始化方法,但是,Xavier推导的时候假设激活函数在零点附近是线性的,显然我们目前常用的ReLU和PReLU并不满足这一条件。所以MSRA初始化主要是想解决使用ReLU激活函数后,方差会发生变化,因此初始化权重的方法也应该变化。
|
||||
|
||||
只考虑输入个数时,MSRA初始化是一个均值为0,方差为2/n的高斯分布,适合于ReLU激活函数:
|
||||
|
||||
$$
|
||||
W \sim N
|
||||
\begin{pmatrix}
|
||||
0, \sqrt{\frac{2}{n}}
|
||||
\end{pmatrix}
|
||||
$$
|
||||
|
||||
$$
|
||||
W \sim U
|
||||
\begin{pmatrix}
|
||||
-\sqrt{\frac{6}{n_{in}}}, \sqrt{\frac{6}{n_{out}}}
|
||||
\end{pmatrix}
|
||||
$$
|
||||
|
||||
图15-4中的激活值从0到1的分布,在各层都非常均匀,不会由于层的加深而梯度消失,所以,在使用ReLU时,推荐使用MSRA法初始化。
|
||||
|
||||
<img src="./img/15/init_msra_relu.png" ch="500" />
|
||||
|
||||
图15-4 MSRA初始化在ReLU激活函数上的表现
|
||||
|
||||
对于Leaky ReLU:
|
||||
|
||||
$$
|
||||
W \sim N \begin{bmatrix} 0, \sqrt{\frac{2}{(1+\alpha^2) \hat n_i}} \end{bmatrix}
|
||||
\\\\ \hat n_i = h_i \cdot w_i \cdot d_i
|
||||
\\\\ h_i: 卷积核高度,w_i: 卷积核宽度,d_i: 卷积核个数
|
||||
$$
|
||||
|
||||
### 15.1.5 小结
|
||||
|
||||
表15-2 几种初始化方法的应用场景
|
||||
|
||||
|ID|网络深度|初始化方法|激活函数|说明|
|
||||
|---|---|---|---|---|
|
||||
|1|单层|零初始化|无|可以|
|
||||
|2|双层|零初始化|Sigmoid|错误,不能进行正确的反向传播|
|
||||
|3|双层|随机初始化|Sigmoid|可以|
|
||||
|4|多层|随机初始化|Sigmoid|激活值分布成凹形,不利于反向传播|
|
||||
|5|多层|Xavier初始化|Tanh|正确|
|
||||
|6|多层|Xavier初始化|ReLU|激活值分布偏向0,不利于反向传播|
|
||||
|7|多层|MSRA初始化|ReLU|正确|
|
||||
|
||||
从表15-2可以看到,由于网络深度和激活函数的变化,使得人们不断地研究新的初始化方法来适应,最终得到1、3、5、7这几种组合。
|
||||
|
||||
### 代码位置
|
||||
|
||||
|
|
|
@ -5,180 +5,10 @@
|
|||
|
||||
### 15.2.1 随机梯度下降 SGD
|
||||
|
||||
先回忆一下随机梯度下降的基本算法,便于和后面的各种算法比较。图15-5中的梯度搜索轨迹为示意图。
|
||||
|
||||
<img src="./img/15/sgd_algorithm.png" />
|
||||
|
||||
图15-5 随机梯度下降算法的梯度搜索轨迹示意图
|
||||
|
||||
#### 输入和参数
|
||||
|
||||
- $\eta$ - 全局学习率
|
||||
|
||||
#### 算法
|
||||
|
||||
---
|
||||
|
||||
计算梯度:$g_t = \nabla_\theta J(\theta_{t-1})$
|
||||
|
||||
更新参数:$\theta_t = \theta_{t-1} - \eta \cdot g_t$
|
||||
|
||||
---
|
||||
|
||||
随机梯度下降算法,在当前点计算梯度,根据学习率前进到下一点。到中点附近时,由于样本误差或者学习率问题,会发生来回徘徊的现象,很可能会错过最优解。
|
||||
|
||||
#### 实际效果
|
||||
|
||||
表15-3 学习率对SGD的影响
|
||||
|
||||
|学习率|损失函数与准确率|
|
||||
|---|---|
|
||||
|0.1|<img src="..\Images\15\op_sgd_ch09_loss_01.png">|
|
||||
|0.3|<img src="..\Images\15\op_sgd_ch09_loss_03.png">|
|
||||
|
||||
SGD的另外一个缺点就是收敛速度慢,见表15-3,在学习率为0.1时,训练10000个epoch不能收敛到预定损失值;学习率为0.3时,训练5000个epoch可以收敛到预定水平。
|
||||
|
||||
### 15.2.2 动量算法 Momentum
|
||||
|
||||
SGD方法的一个缺点是其更新方向完全依赖于当前batch计算出的梯度,因而十分不稳定,因为数据有噪音。
|
||||
|
||||
Momentum算法借用了物理中的动量概念,它模拟的是物体运动时的惯性,即更新的时候在一定程度上保留之前更新的方向,同时利用当前batch的梯度微调最终的更新方向。这样一来,可以在一定程度上增加稳定性,从而学习地更快,并且还有一定摆脱局部最优的能力。Momentum算法会观察历史梯度,若当前梯度的方向与历史梯度一致(表明当前样本不太可能为异常点),则会增强这个方向的梯度。若当前梯度与历史梯度方向不一致,则梯度会衰减。
|
||||
|
||||
<img src="./img/15/momentum_algorithm.png" />
|
||||
|
||||
图15-6 动量算法的前进方向
|
||||
|
||||
图15-6中,第一次的梯度更新完毕后,会记录$v_1$的动量值。在“求梯度点”进行第二次梯度检查时,得到2号方向,与$v_1$的动量组合后,最终的更新为2'方向。这样一来,由于有$v_1$的存在,会迫使梯度更新方向具备“惯性”,从而可以减小随机样本造成的震荡。
|
||||
|
||||
#### 输入和参数
|
||||
|
||||
- $\eta$ - 全局学习率
|
||||
- $\alpha$ - 动量参数,一般取值为0.5, 0.9, 0.99
|
||||
- $v_t$ - 当前时刻的动量,初值为0
|
||||
|
||||
#### 算法
|
||||
|
||||
---
|
||||
|
||||
计算梯度:$g_t = \nabla_\theta J(\theta_{t-1})$
|
||||
|
||||
计算速度更新:$v_t = \alpha \cdot v_{t-1} + \eta \cdot g_t$ (公式1)
|
||||
|
||||
更新参数:$\theta_t = \theta_{t-1} - v_t$ (公式2)
|
||||
|
||||
---
|
||||
|
||||
但是在花书上的公式是这样的:
|
||||
|
||||
---
|
||||
|
||||
$v_t = \alpha \cdot v_{t-1} - \eta \cdot g_t (公式3)$
|
||||
|
||||
$\theta_{t} = \theta_{t-1} + v_t (公式4)$
|
||||
|
||||
---
|
||||
|
||||
这两个差别好大啊!一个加减号错会导致算法不工作!为了搞清楚,咱们手推一下迭代过程。
|
||||
|
||||
根据算法公式(1)(2),以$W$参数为例,有:
|
||||
|
||||
0. $v_0 = 0$
|
||||
1. $dW_0 = \nabla J(w)$
|
||||
2. $v_1 = \alpha v_0 + \eta \cdot dW_0 = \eta \cdot dW_0$
|
||||
3. $W_1 = W_0 - v_1=W_0 - \eta \cdot dW_0$
|
||||
4. $dW_1 = \nabla J(w)$
|
||||
5. $v_2 = \alpha v_1 + \eta dW_1$
|
||||
6. $W_2 = W_1 - v_2 = W_1 - (\alpha v_1 +\eta dW_1) = W_1 - \alpha \cdot \eta \cdot dW_0 - \eta \cdot dW_1$
|
||||
7. $dW_2 = \nabla J(w)$
|
||||
8. $v_3=\alpha v_2 + \eta dW_2$
|
||||
9. $W_3 = W_2 - v_3=W_2-(\alpha v_2 + \eta dW_2) = W_2 - \alpha^2 \eta dW_0 - \alpha \eta dW_1 - \eta dW_2$
|
||||
|
||||
|
||||
根据公式(3)(4)有:
|
||||
|
||||
0. $v_0 = 0$
|
||||
1. $dW_0 = \nabla J(w)$
|
||||
2. $v_1 = \alpha v_0 - \eta \cdot dW_0 = -\eta \cdot dW_0$
|
||||
3. $W_1 = W_0 + v_1=W_0 - \eta \cdot dW_0$
|
||||
4. $dW_1 = \nabla J(w)$
|
||||
5. $v_2 = \alpha v_1 - \eta dW_1$
|
||||
6. $W_2 = W_1 + v_2 = W_1 + (\alpha v_1 - \eta dW_1) = W_1 - \alpha \cdot \eta \cdot dW_0 - \eta \cdot dW_1$
|
||||
7. $dW_2 = \nabla J(w)$
|
||||
8. $v_3=\alpha v_2 - \eta dW_2$
|
||||
9. $W_3 = W_2 + v_3=W_2 + (\alpha v_2 - \eta dW_2) = W_2 - \alpha^2 \eta dW_0 - \alpha \eta dW_1-\eta dW_2$
|
||||
|
||||
通过手工推导迭代,我们得到两个结论:
|
||||
|
||||
1. 可以看到两种方式的第9步结果是相同的,即公式(1)(2)等同于(3)(4)
|
||||
2. 与普通SGD的算法$W_3 = W_2 - \eta dW_2$相比,动量法不但每次要减去当前梯度,还要减去历史梯度$W_0,W_1$乘以一个不断减弱的因子$\alpha$,因为$\alpha$小于1,所以$\alpha^2$比$\alpha$小,$\alpha^3$比$\alpha^2$小。这种方式的学名叫做指数加权平均。
|
||||
|
||||
#### 实际效果
|
||||
|
||||
表15-4 SGD和动量法的比较
|
||||
|
||||
|算法|损失函数和准确率|
|
||||
|---|---|
|
||||
|SGD|<img src="..\Images\15\op_sgd_ch09_loss_01.png">|
|
||||
|Momentum|<img src="..\Images\15\op_momentum_ch09_loss_01.png">|
|
||||
|
||||
从表15-4的比较可以看到,使用同等的超参数设置,普通梯度下降算法经过epoch=10000次没有到达预定0.001的损失值;动量算法经过2000个epoch迭代结束。
|
||||
|
||||
在损失函数历史数据图中,中间有一大段比较平坦的区域,梯度值很小,或者是随机梯度下降算法找不到合适的方向前进,只能慢慢搜索。而下侧的动量法,利用惯性,判断当前梯度与上次梯度的关系,如果方向相同,则会加速前进;如果不同,则会减速,并趋向平衡。所以很快地就达到了停止条件。
|
||||
|
||||
当我们将一个小球从山上滚下来时,没有阻力的话,它的动量会越来越大,但是如果遇到了阻力,速度就会变小。加入的这一项,可以使得梯度方向不变的维度上速度变快,梯度方向有所改变的维度上的更新速度变慢,这样就可以加快收敛并减小震荡。
|
||||
|
||||
### 15.2.3 梯度加速算法 NAG
|
||||
|
||||
Nesterov Accelerated Gradient,或者叫做Nesterov Momentum。
|
||||
|
||||
在小球向下滚动的过程中,我们希望小球能够提前知道在哪些地方坡面会上升,这样在遇到上升坡面之前,小球就开始减速。这方法就是Nesterov Momentum,其在凸优化中有较强的理论保证收敛。并且,在实践中Nesterov Momentum也比单纯的Momentum 的效果好。
|
||||
|
||||
#### 输入和参数
|
||||
|
||||
- $\eta$ - 全局学习率
|
||||
- $\alpha$ - 动量参数,缺省取值0.9
|
||||
- $v$ - 动量,初始值为0
|
||||
|
||||
#### 算法
|
||||
|
||||
---
|
||||
|
||||
临时更新:$\hat \theta = \theta_{t-1} - \alpha \cdot v_{t-1}$
|
||||
|
||||
前向计算:$f(\hat \theta)$
|
||||
|
||||
计算梯度:$g_t = \nabla_{\hat\theta} J(\hat \theta)$
|
||||
|
||||
计算速度更新:$v_t = \alpha \cdot v_{t-1} + \eta \cdot g_t$
|
||||
|
||||
更新参数:$\theta_t = \theta_{t-1} - v_t$
|
||||
|
||||
---
|
||||
|
||||
其核心思想是:注意到 momentum 方法,如果只看 $\alpha \cdot v_{t-1}$ 项,那么当前的θ经过momentum的作用会变成 $\theta - \alpha \cdot v_{t-1}$。既然我们已经知道了下一步的走向,我们不妨先走一步,到达新的位置”展望”未来,然后在新位置上求梯度, 而不是原始的位置。
|
||||
|
||||
所以,同Momentum相比,梯度不是根据当前位置θ计算出来的,而是在移动之后的位置$\theta - \alpha \cdot v_{t-1}$计算梯度。理由是,既然已经确定会移动$\theta - \alpha \cdot v_{t-1}$,那不如之前去看移动后的梯度。
|
||||
|
||||
图15-7是NAG的前进方向。
|
||||
|
||||
<img src="./img/15/nag_algorithm.png" ch="500" />
|
||||
|
||||
图15-7 梯度加速算法的前进方向
|
||||
|
||||
这个改进的目的就是为了提前看到前方的梯度。如果前方的梯度和当前梯度目标一致,那我直接大步迈过去; 如果前方梯度同当前梯度不一致,那我就小心点更新。
|
||||
|
||||
#### 实际效果
|
||||
|
||||
表15-5 动量法和NAG法的比较
|
||||
|
||||
|算法|损失函数和准确率|
|
||||
|---|---|
|
||||
|Momentum|<img src="..\Images\15\op_momentum_ch09_loss_01.png">|
|
||||
|NAG|<img src="..\Images\15\op_nag_ch09_loss_01.png">|
|
||||
|
||||
表15-9显示,使用动量算法经过2000个epoch迭代结束,NAG算法是加速的动量法,因此只用1400个epoch迭代结束。
|
||||
|
||||
NAG 可以使 RNN 在很多任务上有更好的表现。
|
||||
|
||||
### 代码位置
|
||||
|
||||
|
|
|
@ -3,207 +3,16 @@
|
|||
|
||||
## 15.3 自适应学习率算法
|
||||
|
||||
以下为本小节目录,详情请参阅《智能之门》正版图书,高等教育出版社。
|
||||
|
||||
### 15.3.1 AdaGrad
|
||||
|
||||
Adaptive subgradient method.$^{[1]}$
|
||||
|
||||
AdaGrad是一个基于梯度的优化算法,它的主要功能是:它对不同的参数调整学习率,具体而言,对低频出现的参数进行大的更新,对高频出现的参数进行小的更新。因此,他很适合于处理稀疏数据。
|
||||
|
||||
在这之前,我们对于所有的参数使用相同的学习率进行更新。但 Adagrad 则不然,对不同的训练迭代次数t,AdaGrad 对每个参数都有一个不同的学习率。这里开方、除法和乘法的运算都是按元素运算的。这些按元素运算使得目标函数自变量中每个元素都分别拥有自己的学习率。
|
||||
|
||||
#### 输入和参数
|
||||
|
||||
- $\eta$ - 全局学习率
|
||||
- $\epsilon$ - 用于数值稳定的小常数,建议缺省值为`1e-6`
|
||||
- $r=0$ 初始值
|
||||
|
||||
#### 算法
|
||||
|
||||
---
|
||||
|
||||
计算梯度:$g_t = \nabla_\theta J(\theta_{t-1})$
|
||||
|
||||
累计平方梯度:$r_t = r_{t-1} + g_t \odot g_t$
|
||||
|
||||
计算梯度更新:$\Delta \theta = {\eta \over \epsilon + \sqrt{r_t}} \odot g_t$
|
||||
|
||||
更新参数:$\theta_t=\theta_{t-1} - \Delta \theta$
|
||||
|
||||
---
|
||||
|
||||
从AdaGrad算法中可以看出,随着算法不断迭代,$r$会越来越大,整体的学习率会越来越小。所以,一般来说AdaGrad算法一开始是激励收敛,到了后面就慢慢变成惩罚收敛,速度越来越慢。$r$值的变化如下:
|
||||
|
||||
0. $r_0 = 0$
|
||||
1. $r_1=g_1^2$
|
||||
2. $r_2=g_1^2+g_2^2$
|
||||
3. $r_3=g_1^2+g_2^2+g_3^2$
|
||||
|
||||
在SGD中,随着梯度的增大,我们的学习步长应该是增大的。但是在AdaGrad中,随着梯度$g$的增大,$r$也在逐渐的增大,且在梯度更新时$r$在分母上,也就是整个学习率是减少的,这是为什么呢?
|
||||
|
||||
这是因为随着更新次数的增大,我们希望学习率越来越慢。因为我们认为在学习率的最初阶段,我们距离损失函数最优解还很远,随着更新次数的增加,越来越接近最优解,所以学习率也随之变慢。
|
||||
|
||||
但是当某个参数梯度较小时,累积和也会小,那么更新速度就大。
|
||||
|
||||
经验上已经发现,对于训练深度神经网络模型而言,从训练开始时积累梯度平方会导致有效学习率过早和过量的减小。AdaGrad在某些深度学习模型上效果不错,但不是全部。
|
||||
|
||||
#### 实际效果
|
||||
|
||||
表15-6 AdaGrad算法的学习率设置
|
||||
|
||||
|初始学习率|损失函数值变化|
|
||||
|---|---|
|
||||
|eta=0.3|<img src="..\Images\15\op_adagrad_ch09_loss_03.png">|
|
||||
|eta=0.5|<img src="..\Images\15\op_adagrad_ch09_loss_05.png">|
|
||||
|eta=0.7|<img src="..\Images\15\op_adagrad_ch09_loss_07.png">|
|
||||
|
||||
表15-6表明,我们设定不同的初始学习率,分别为0.3、0.5、0.7,可以看到学习率为0.7时,收敛得最快,只用1750个epoch;学习率为0.5时用了3000个epoch;学习率为0.3时用了8000个epoch。所以,对于AdaGrad来说,可以在开始时把学习率的值设置大一些,因为它会衰减得很快。
|
||||
|
||||
### 15.3.2 AdaDelta
|
||||
|
||||
Adaptive Learning Rate Method. $^{[2]}$
|
||||
|
||||
AdaDelta法是AdaGrad 法的一个延伸,它旨在解决它学习率不断单调下降的问题。相比计算之前所有梯度值的平方和,AdaDelta法仅计算在一个大小为w的时间区间内梯度值的累积和。
|
||||
|
||||
但该方法并不会存储之前梯度的平方值,而是将梯度值累积值按如下的方式递归地定义:关于过去梯度值的衰减均值,当前时间的梯度均值是基于过去梯度均值和当前梯度值平方的加权平均,其中是类似上述动量项的权值。
|
||||
|
||||
#### 输入和参数
|
||||
|
||||
- $\epsilon$ - 用于数值稳定的小常数,建议缺省值为1e-5
|
||||
- $\alpha \in [0,1)$ - 衰减速率,建议0.9
|
||||
- $s$ - 累积变量,初始值0
|
||||
- $r$ - 累积变量变化量,初始为0
|
||||
|
||||
#### 算法
|
||||
|
||||
---
|
||||
|
||||
计算梯度:$g_t = \nabla_\theta J(\theta_{t-1})$
|
||||
|
||||
累积平方梯度:$s_t = \alpha \cdot s_{t-1} + (1-\alpha) \cdot g_t \odot g_t$
|
||||
|
||||
计算梯度更新:$\Delta \theta = \sqrt{r_{t-1} + \epsilon \over s_t + \epsilon} \odot g_t$
|
||||
|
||||
更新梯度:$\theta_t = \theta_{t-1} - \Delta \theta$
|
||||
|
||||
更新变化量:$r = \alpha \cdot r_{t-1} + (1-\alpha) \cdot \Delta \theta \odot \Delta \theta$
|
||||
|
||||
---
|
||||
|
||||
#### 实际效果
|
||||
|
||||
表15-7 AdaDelta法的学习率设置
|
||||
|
||||
|初始学习率|损失函数值|
|
||||
|---|---|
|
||||
|eta=0.1|<img src="..\Images\15\op_adadelta_ch09_loss_01.png">|
|
||||
|eta=0.01|<img src="..\Images\15\op_adadelta_ch09_loss_001.png">|
|
||||
|
||||
从表15-7可以看到,初始学习率设置为0.1或者0.01,对于本算法来说都是一样的,这是因为算法中用r来代替学习率。
|
||||
|
||||
### 15.3.3 均方根反向传播 RMSProp
|
||||
|
||||
Root Mean Square Prop。$^{[3]}$
|
||||
|
||||
RMSprop 是由 Geoff Hinton 在他 Coursera 课程中提出的一种适应性学习率方法,至今仍未被公开发表。RMSprop法要解决AdaGrad的学习率缩减问题。
|
||||
|
||||
#### 输入和参数
|
||||
|
||||
- $\eta$ - 全局学习率,建议设置为0.001
|
||||
- $\epsilon$ - 用于数值稳定的小常数,建议缺省值为1e-8
|
||||
- $\alpha$ - 衰减速率,建议缺省取值0.9
|
||||
- $r$ - 累积变量矩阵,与$\theta$尺寸相同,初始化为0
|
||||
|
||||
#### 算法
|
||||
|
||||
---
|
||||
|
||||
计算梯度:$g_t = \nabla_\theta J(\theta_{t-1})$
|
||||
|
||||
累计平方梯度:$r = \alpha \cdot r + (1-\alpha)(g_t \odot g_t)$
|
||||
|
||||
计算梯度更新:$\Delta \theta = {\eta \over \sqrt{r + \epsilon}} \odot g_t$
|
||||
|
||||
更新参数:$\theta_{t}=\theta_{t-1} - \Delta \theta$
|
||||
|
||||
---
|
||||
|
||||
RMSprop也将学习率除以了一个指数衰减的衰减均值。为了进一步优化损失函数在更新中存在摆动幅度过大的问题,并且进一步加快函数的收敛速度,RMSProp算法对权重$W$和偏置$b$的梯度使用了微分平方加权平均数,这种做法有利于消除了摆动幅度大的方向,用来修正摆动幅度,使得各个维度的摆动幅度都较小。另一方面也使得网络函数收敛更快。
|
||||
|
||||
其中,$r$值的变化如下:
|
||||
|
||||
0. $r_0 = 0$
|
||||
1. $r_1=0.1g_1^2$
|
||||
2. $r_2=0.9r_1+0.1g_2^2=0.09g_1^2+0.1g_2^2$
|
||||
3. $r_3=0.9r_2+0.1g_3^2=0.081g_1^2+0.09g_2^2+0.1g_3^2$
|
||||
|
||||
与AdaGrad相比,$r_3$要小很多,那么计算出来的学习率也不会衰减的太厉害。注意,在计算梯度更新时,分母开始时是个小于1的数,而且非常小,所以如果全局学习率设置过大的话,比如0.1,将会造成开始的步子迈得太大,而且久久不能收缩步伐,损失值也降不下来。
|
||||
|
||||
#### 实际效果
|
||||
|
||||
表15-8 RMSProp的学习率设置
|
||||
|
||||
|初始学习率|损失函数值|
|
||||
|---|---|
|
||||
|eta=0.1|<img src="..\Images\15\op_rmsprop_ch09_loss_01.png">|<
|
||||
||迭代了10000次,损失值一直在0.005下不来,说明初始学习率太高了,需要给一个小一些的初值|
|
||||
|eta=0.01|<img src="..\Images\15\op_rmsprop_ch09_loss_001.png">|
|
||||
||合适的学习率初值设置||
|
||||
|eta=0.005|<img src="..\Images\15\op_rmsprop_ch09_loss_0005.png">|
|
||||
||初值稍微小了些,造成迭代次数增加才能到达精度要求||
|
||||
|
||||
从上面的试验可以看出,0.01是本示例最好的设置。
|
||||
|
||||
|
||||
### 15.3.4 Adam - Adaptive Moment Estimation
|
||||
|
||||
计算每个参数的自适应学习率,相当于RMSProp + Momentum的效果,Adam$^{[4]}$算法在RMSProp算法基础上对小批量随机梯度也做了指数加权移动平均。和AdaGrad算法、RMSProp算法以及AdaDelta算法一样,目标函数自变量中每个元素都分别拥有自己的学习率。
|
||||
|
||||
#### 输入和参数
|
||||
|
||||
- $t$ - 当前迭代次数
|
||||
- $\eta$ - 全局学习率,建议缺省值为0.001
|
||||
- $\epsilon$ - 用于数值稳定的小常数,建议缺省值为1e-8
|
||||
- $\beta_1, \beta_2$ - 矩估计的指数衰减速率,$\in[0,1)$,建议缺省值分别为0.9和0.999
|
||||
|
||||
#### 算法
|
||||
|
||||
---
|
||||
|
||||
计算梯度:$g_t = \nabla_\theta J(\theta_{t-1})$
|
||||
|
||||
计数器加一:$t=t+1$
|
||||
|
||||
更新有偏一阶矩估计:$m_t = \beta_1 \cdot m_{t-1} + (1-\beta_1) \cdot g_t$
|
||||
|
||||
更新有偏二阶矩估计:$v_t = \beta_2 \cdot v_{t-1} + (1-\beta_2)(g_t \odot g_t)$
|
||||
|
||||
修正一阶矩的偏差:$\hat m_t = m_t / (1-\beta_1^t)$
|
||||
|
||||
修正二阶矩的偏差:$\hat v_t = v_t / (1-\beta_2^t)$
|
||||
|
||||
计算梯度更新:$\Delta \theta = \eta \cdot \hat m_t /(\epsilon + \sqrt{\hat v_t})$
|
||||
|
||||
更新参数:$\theta_t=\theta_{t-1} - \Delta \theta$
|
||||
|
||||
---
|
||||
|
||||
#### 实际效果
|
||||
|
||||
表15-9 Adam法的学习率设置
|
||||
|
||||
|初始学习率|损失函数值|
|
||||
|---|---|
|
||||
|eta=0.1|<img src="..\Images\15\op_adam_ch09_loss_01.png">|
|
||||
||迭代了10000次,但是损失值没有降下来,因为初始学习率0.1太高了|
|
||||
|eta=0.01|<img src="..\Images\15\op_adam_ch09_loss_001.png">|
|
||||
||比较合适的学习率|
|
||||
|eta=0.005|<img src="..\Images\15\op_adam_ch09_loss_0005.png">|
|
||||
||学习率较低|
|
||||
|eta=0.001|<img src="..\Images\15\op_adam_ch09_loss_0001.png">|
|
||||
||初始学习率太低,收敛到目标损失值的速度慢|
|
||||
|
||||
由于Adam继承了RMSProp的传统,所以学习率不宜设置太高,从表15-9的比较可以看到,初始学习率设置为0.01时比较理想。
|
||||
|
||||
### 代码位置
|
||||
|
||||
ch15, Level3
|
||||
|
|
|
@ -3,119 +3,12 @@
|
|||
|
||||
## 15.4 算法在等高线图上的效果比较
|
||||
|
||||
以下为本小节目录,详情请参阅《智能之门》正版图书,高等教育出版社。
|
||||
|
||||
### 15.4.1 模拟效果比较
|
||||
|
||||
为了简化起见,我们先用一个简单的二元二次函数来模拟损失函数的等高线图,测试一下我们在前面实现的各种优化器。但是以下测试结果只是一个示意性质的,可以理解为在绝对理想的条件下(样本无噪音,损失函数平滑等等)的各算法的表现。
|
||||
|
||||
$$z = \frac{x^2}{10} + y^2 \tag{1}$$
|
||||
|
||||
公式1是模拟均方差函数的形式,它的正向计算和反向计算的`Python`代码如下:
|
||||
|
||||
```Python
|
||||
def f(x, y):
|
||||
return x**2 / 10.0 + y**2
|
||||
|
||||
def derivative_f(x, y):
|
||||
return x / 5.0, 2.0*y
|
||||
```
|
||||
|
||||
我们依次测试4种方法:
|
||||
|
||||
- 普通SGD, 学习率0.95
|
||||
- 动量Momentum, 学习率0.1
|
||||
- RMPSProp,学习率0.5
|
||||
- Adam,学习率0.5
|
||||
|
||||
每种方法都迭代20次,记录下每次反向过程的(x,y)坐标点,绘制图15-8如下。
|
||||
|
||||
<img src="./img/15/Optimizers_sample.png" ch="500" />
|
||||
|
||||
图15-8 不同梯度下降优化算法的模拟比较
|
||||
|
||||
- SGD算法,每次迭代完全受当前梯度的控制,所以会以折线方式前进。
|
||||
- Momentum算法,学习率只有0.1,每次继承上一次的动量方向,所以会以比较平滑的曲线方式前进,不会出现突然的转向。
|
||||
- RMSProp算法,有历史梯度值参与做指数加权平均,所以可以看到比较平缓,不会波动太大,都后期步长越来越短也是符合学习规律的。
|
||||
- Adam算法,因为可以被理解为Momentum和RMSProp的组合,所以比Momentum要平缓一些,比RMSProp要平滑一些。
|
||||
|
||||
### 15.4.2 真实效果比较
|
||||
|
||||
下面我们用第四章线性回归的例子来做实际的测试。为什么要用线性回归的例子呢?因为在它只有w, b两个变量需要求解,可以在二维平面或三维空间来表现,这样我们就可以用可视化的方式来解释算法的效果。
|
||||
|
||||
下面列出了用`Python`代码实现的前向计算、反向计算、损失函数计算的函数:
|
||||
|
||||
```Python
|
||||
def ForwardCalculationBatch(W,B,batch_x):
|
||||
Z = np.dot(W, batch_x) + B
|
||||
return Z
|
||||
|
||||
def BackPropagationBatch(batch_x, batch_y, batch_z):
|
||||
m = batch_x.shape[1]
|
||||
dZ = batch_z - batch_y
|
||||
dB = dZ.sum(axis=1, keepdims=True)/m
|
||||
dW = np.dot(dZ, batch_x.T)/m
|
||||
return dW, dB
|
||||
|
||||
def CheckLoss(W, B, X, Y):
|
||||
m = X.shape[1]
|
||||
Z = np.dot(W, X) + B
|
||||
LOSS = (Z - Y)**2
|
||||
loss = LOSS.sum()/m/2
|
||||
return loss
|
||||
```
|
||||
|
||||
损失函数用的是均方差,回忆一下公式:
|
||||
|
||||
$$J(w,b) = \frac{1}{2}(Z-Y)^2 \tag{2}$$
|
||||
|
||||
如果把公式2展开的话:
|
||||
|
||||
$$J = \frac{1}{2} (Z^2 + Y^2 - 2ZY)$$
|
||||
|
||||
其形式比公式1多了最后一项,所以画出来的损失函数的等高线是斜向的椭圆。下面是画等高线的代码方法,详情请移步代码库:
|
||||
|
||||
```Python
|
||||
def show_contour(ax, loss_history, optimizer):
|
||||
```
|
||||
|
||||
这里有个`matplotlib`的绘图知识:
|
||||
|
||||
1. 确定`x_axis`值的范围:`w = np.arange(1,3,0.01)`,因为`w`的准确值是2
|
||||
2. 确定`y_axis`值的范围:`b = np.arange(2,4,0.01)`,因为`b`的准确值是3
|
||||
3. 生成网格数据:`W,B = np.meshgrid(w, b)`
|
||||
4. 计算每个网点上的损失函数值Z
|
||||
5. 所以(W,B,Z)形成了一个3D图,最后用`ax.coutour(W,B,Z)`来绘图
|
||||
6. `levels`参数是控制等高线的精度或密度,`norm`控制颜色的非线性变化
|
||||
|
||||
表15-10 各种算法的效果比较
|
||||
|
||||
|||
|
||||
|---|---|
|
||||
|<img src="..\Images\15\op_sgd_ch04.png">|<img src="..\Images\15\op_sgd2_ch04.png">|
|
||||
|SGD当学习率为0.1时,需要很多次迭代才能逐渐向中心靠近|SGD当学习率为0.5时,会比较快速地向中心靠近,但是在中心的附近有较大震荡|
|
||||
|<img src="..\Images\15\op_momentum_ch04.png">|<img src="..\Images\15\op_nag_ch04.png">|
|
||||
|Momentum由于惯性存在,一下子越过了中心点,但是很快就会得到纠正|Nag是Momentum的改进,有预判方向功能|
|
||||
|<img src="..\Images\15\op_adagrad_ch04.png">|<img src="..\Images\15\op_adadelta_ch04.png">|
|
||||
|AdaGrad的学习率在开始时可以设置大一些,因为会很快衰减|AdaDelta即使把学习率设置为0,也不会影响,因为有内置的学习率策略|
|
||||
|<img src="..\Images\15\op_rmsprop_ch04.png">|<img src="..\Images\15\op_adam_ch04.png">|
|
||||
|RMSProp解决AdaGrad的学习率下降问题,即使学习率设置为0.1,收敛也会快|Adam到达中点的路径比较直接|
|
||||
|
||||
在表15-10中,观察其中4组优化器的训练轨迹:
|
||||
|
||||
- SGD:在较远的地方,沿梯度方向下降,越靠近中心的地方,抖动得越多,似乎找不准方向,得到loss值等于0.005迭代了148次。
|
||||
- Momentum:由于惯性存在,一下子越过了中心点,但是很快就会得到纠正,得到loss值等于0.005迭代了128次。
|
||||
- RMSProp:与SGD的行为差不多,抖动大,得到loss值等于0.005迭代了130次。
|
||||
- Adam:与Momentum一样,越过中心点,但后来的收敛很快,得到loss值等于0.005迭代了107次。
|
||||
|
||||
为了能看清最后几步的行为,我们放大每张图,如图15-9所示,再看一下。
|
||||
|
||||
<img src="./img/15/Optimizers_zoom.png" ch="500" />
|
||||
|
||||
图15-9 放大后各优化器的训练轨迹
|
||||
|
||||
- SGD:接近中点的过程很曲折,步伐很慢,甚至有反方向的,容易陷入局部最优。
|
||||
- Momentum:快速接近中点,但中间跳跃较大。
|
||||
- RMSProp:接近中点很曲折,但是没有反方向的,用的步数比SGD少,跳动较大,有可能摆脱局部最优解的。
|
||||
- Adam:快速接近中点,难怪很多人喜欢用这个优化器。
|
||||
|
||||
### 代码位置
|
||||
|
||||
|
|
|
@ -3,192 +3,19 @@
|
|||
|
||||
## 15.5 批量归一化的原理
|
||||
|
||||
有的书翻译成归一化,有的翻译成正则化,英文Batch Normalization,简称为BatchNorm,或BN。
|
||||
以下为本小节目录,详情请参阅《智能之门》正版图书,高等教育出版社。
|
||||
|
||||
### 15.5.1 基本数学知识
|
||||
|
||||
#### 正态分布
|
||||
|
||||
正态分布,又叫做高斯分布。
|
||||
|
||||
若随机变量$X$,服从一个位置参数为$\mu$、尺度参数为$\sigma$的概率分布,且其概率密度函数为:
|
||||
|
||||
$$
|
||||
f(x)=\frac{1}{\sigma\sqrt{2 \pi} } e^{- \frac{{(x-\mu)^2}}{2\sigma^2}} \tag{1}
|
||||
$$
|
||||
|
||||
则这个随机变量就称为正态随机变量,正态随机变量服从的分布就称为正态分布,记作:
|
||||
|
||||
$$
|
||||
X \sim N(\mu,\sigma^2) \tag{2}
|
||||
$$
|
||||
|
||||
当μ=0,σ=1时,称为标准正态分布:
|
||||
|
||||
$$X \sim N(0,1) \tag{3}$$
|
||||
|
||||
此时公式简化为:
|
||||
|
||||
$$
|
||||
f(x)=\frac{1}{\sqrt{2 \pi}} e^{- \frac{x^2}{2}} \tag{4}
|
||||
$$
|
||||
|
||||
图15-10就是三种($\mu,\sigma$)组合的函数图像。
|
||||
|
||||
<img src="./img/15/bn1.png" ch="500" />
|
||||
|
||||
图15-10 不同参数的正态分布函数曲线
|
||||
|
||||
### 15.5.2 深度神经网络的挑战
|
||||
|
||||
机器学习领域有个很重要的假设:I.I.D.(独立同分布)假设,就是假设训练数据和测试数据是满足相同分布的,这样就能做到通过训练数据获得的模型能够在测试集获得好的效果。
|
||||
|
||||
在深度神经网络中,我们可以将每一层视为对输入的信号做了一次变换:
|
||||
|
||||
$$
|
||||
Z = W \cdot X + B \tag{5}
|
||||
$$
|
||||
|
||||
我们在第5章学过,输入层的数据已经归一化,如果不做归一化,很多时候甚至网络不会收敛,可见归一化的重要性。
|
||||
|
||||
随后的网络的每一层的输入数据在经过公式5的运算后,其分布一直在发生变化,前面层训练参数的更新将导致后面层输入数据分布的变化,必然会引起后面每一层输入数据分布的改变,不再是输入的原始数据所适应的分布了。
|
||||
|
||||
而且,网络前面几层微小的改变,后面几层就会逐步把这种改变累积放大。训练过程中网络中间层数据分布的改变称之为内部协变量偏移(Internal Covariate Shift)。BN的提出,就是要解决在训练过程中,中间层数据分布发生改变的情况。
|
||||
|
||||
比如,在上图中,假设X是服从蓝色或红色曲线的分布,经过公式5后,有可能变成了绿色曲线的分布。
|
||||
|
||||
标准正态分布的数值密度占比如图15-11所示。
|
||||
|
||||
<img src="./img/15/bn2.png" ch="500" />
|
||||
|
||||
图15-11 标准正态分布的数值密度占比
|
||||
|
||||
有68%的值落在[-1,1]之间,有95%的值落在[-2,2]之间。
|
||||
|
||||
比较一下偏移后的数据分布区域和Sigmoid激活函数的图像,如图15-12所示。
|
||||
|
||||
<img src="./img/15/bn3.png" ch="500" />
|
||||
|
||||
图15-12 偏移后的数据分布区域和Sigmoid激活函数
|
||||
|
||||
可以看到带来的问题是:
|
||||
|
||||
1. 在大于2的区域,激活后的值基本接近1了,饱和输出。如果蓝色曲线表示的数据更偏向右侧的话,激活函数就会失去了作用,因为所有的输出值都是0.94、0.95、0.98这样子的数值,区别不大;
|
||||
2. 导数数值小,只有不到0.1甚至更小,反向传播的力度很小,网络很难收敛。
|
||||
|
||||
有的人会问,我们在深度学习中不是都用ReLU激活函数吗?那么BN对于ReLU有用吗?下面我们看看ReLU函数的图像,如图15-13所示。
|
||||
|
||||
<img src="./img/15/bn4.png" ch="500" />
|
||||
|
||||
图15-13 ReLU函数曲线
|
||||
|
||||
上图中蓝色为数据分布,已经从0点向右偏移了,黄色为ReLU的激活值,可以看到95%以上的数据都在大于0的区域,从而被Relu激活函数原封不动第传到了下一层网络中,而没有被小于0的部分剪裁,那么这个网络和线性网络也差不多了,失去了深层网络的能力。
|
||||
|
||||
### 15.5.3 批量归一化
|
||||
|
||||
既然可以把原始训练样本做归一化,那么如果在深度神经网络的每一层,都可以有类似的手段,也就是说把层之间传递的数据移到0点附近,那么训练效果就应该会很理想。这就是批归一化BN的想法的来源。
|
||||
|
||||
深度神经网络随着网络深度加深,训练起来越困难,收敛越来越慢,这是个在DL领域很接近本质的问题。很多论文都是解决这个问题的,比如ReLU激活函数,再比如Residual Network。BN本质上也是解释并从某个不同的角度来解决这个问题的。
|
||||
|
||||
BN就是在深度神经网络训练过程中使得每一层神经网络的输入保持相同的分布,致力于将每一层的输入数据正则化成$N(0,1)$的分布。因次,每次训练的数据必须是mini-batch形式,一般取32,64等数值。
|
||||
|
||||
具体的数据处理过程如图15-14所示。
|
||||
|
||||
<img src="./img/15/bn6.png" ch="500" />
|
||||
|
||||
图15-14 数据处理过程
|
||||
|
||||
1. 数据在训练过程中,在网络的某一层会发生Internal Covariate Shift,导致数据处于激活函数的饱和区;
|
||||
2. 经过均值为0、方差为1的变换后,位移到了0点附近。但是只做到这一步的话,会带来两个问题:
|
||||
|
||||
a. 在[-1,1]这个区域,Sigmoid激活函数是近似线性的,造成激活函数失去非线性的作用;
|
||||
|
||||
b. 在二分类问题中我们学习过,神经网络把正类样本点推向了右侧,把负类样本点推向了左侧,如果再把它们强行向中间集中的话,那么前面学习到的成果就会被破坏;
|
||||
|
||||
3. 经过$\gamma,\beta$的线性变换后,把数据区域拉宽,则激活函数的输出既有线性的部分,也有非线性的部分,这就解决了问题a;而且由于$\gamma,\beta$也是通过网络进行学习的,所以以前学到的成果也会保持,这就解决了问题b。
|
||||
|
||||
在实际的工程中,我们把BN当作一个层来看待,一般架设在全连接层(或卷积层)与激活函数层之间。
|
||||
|
||||
### 15.5.4 前向计算
|
||||
|
||||
#### 符号表
|
||||
|
||||
表15-11中,m表示batch_size的大小,比如32或64个样本/批;n表示features数量,即样本特征值数量。
|
||||
|
||||
表15-11 各个参数的含义和数据形状
|
||||
|
||||
|符号|数据类型|数据形状|
|
||||
|:---------:|:-----------:|:---------:|
|
||||
|$X$| 输入数据矩阵 | [m, n] |
|
||||
|$x_i$|输入数据第i个样本| [1, n] |
|
||||
|$N$| 经过归一化的数据矩阵 | [m, n] |
|
||||
|$n_i$| 经过归一化的单样本 | [1, n] |
|
||||
|$\mu_B$| 批数据均值 | [1, n] |
|
||||
|$\sigma^2_B$| 批数据方差 | [1, n] |
|
||||
|$m$|批样本数量| [1] |
|
||||
|$\gamma$|线性变换参数| [1, n] |
|
||||
|$\beta$|线性变换参数| [1, n] |
|
||||
|$Z$|线性变换后的矩阵| [1, n] |
|
||||
|$z_i$|线性变换后的单样本| [1, n] |
|
||||
|$\delta$| 反向传入的误差 | [m, n] |
|
||||
|
||||
如无特殊说明,以下乘法为元素乘,即element wise的乘法。
|
||||
|
||||
在训练过程中,针对每一个batch数据,m是批的大小。进行的操作是,将这组数据正则化,之后对其进行线性变换。
|
||||
|
||||
具体的算法步骤是:
|
||||
|
||||
$$
|
||||
\mu_B = \frac{1}{m}\sum_1^m x_i \tag{6}
|
||||
$$
|
||||
|
||||
$$
|
||||
\sigma^2_B = \frac{1}{m} \sum_1^m (x_i-\mu_B)^2 \tag{7}
|
||||
$$
|
||||
|
||||
$$
|
||||
n_i = \frac{x_i-\mu_B}{\sqrt{\sigma^2_B + \epsilon}} \tag{8}
|
||||
$$
|
||||
|
||||
$$
|
||||
z_i = \gamma n_i + \beta \tag{9}
|
||||
$$
|
||||
|
||||
其中,$\gamma,\beta$是训练出来的,$\epsilon$是防止$\sigma_B^2$为0时加的一个很小的数值,通常为`1e-5`。
|
||||
|
||||
### 15.5.5 测试和推理时的归一化方法
|
||||
|
||||
批量归一化的“批量”两个字,表示在训练过程中需要有一小批数据,比如32个样本。而在测试过程或推理时,我们只有一个样本的数据,根本没有mini-batch的概念,无法计算算出正确的均值。因此,我们使用的均值和方差数据是在训练过程中样本值的平均。也就是:
|
||||
|
||||
$$
|
||||
E[x] = E[\mu_B]
|
||||
$$
|
||||
$$
|
||||
Var[x] = \frac{m}{m-1} E[\sigma^2_B]
|
||||
$$
|
||||
|
||||
一种做法是,我们把所有批次的$\mu$和$\sigma$都记录下来,然后在最后训练完毕时(或做测试时)平均一下。
|
||||
|
||||
另外一种做法是使用类似动量的方式,训练时,加权平均每个批次的值,权值$\alpha$可以为0.9:
|
||||
|
||||
$$m_{t} = \alpha \cdot m_{t-1} + (1-\alpha) \cdot \mu_t$$
|
||||
$$v_{t} = \alpha \cdot v_{t-1} + (1-\alpha) \cdot \sigma_t$$
|
||||
|
||||
测试或推理时,直接使用$m_t和v_t$的值即可。
|
||||
|
||||
### 15.5.6 批量归一化的优点
|
||||
|
||||
1. 可以选择比较大的初始学习率,让你的训练速度提高。
|
||||
|
||||
以前还需要慢慢调整学习率,甚至在网络训练到一定程度时,还需要想着学习率进一步调小的比例选择多少比较合适,现在我们可以采用初始很大的学习率,因为这个算法收敛很快。当然这个算法即使你选择了较小的学习率,也比以前的收敛速度快,因为它具有快速训练收敛的特性;
|
||||
|
||||
2. 减少对初始化的依赖
|
||||
|
||||
一个不太幸运的初始化,可能会造成网络训练实际很长,甚至不收敛。
|
||||
|
||||
3. 减少对正则的依赖
|
||||
|
||||
在第16章中,我们将会学习正则化知识,以增强网络的泛化能力。采用BN算法后,我们会逐步减少对正则的依赖,比如令人头疼的dropout、L2正则项参数的选择问题,或者可以选择更小的L2正则约束参数了,因为BN具有提高网络泛化能力的特性;
|
||||
|
||||
### 代码位置
|
||||
|
||||
|
|
|
@ -3,247 +3,16 @@
|
|||
|
||||
## 15.6 批量归一化的实现
|
||||
|
||||
在这一节中,我们将会动手实现一个批量归一化层,来验证批量归一化的实际作用。
|
||||
以下为本小节目录,详情请参阅《智能之门》正版图书,高等教育出版社。
|
||||
|
||||
### 15.6.1 反向传播
|
||||
|
||||
在上一节中,我们知道了批量归一化的正向计算过程,这一节中,为了实现完整的批量归一化层,我们首先需要推导它的反向传播公式,然后用代码实现。本节中的公式序号接上一节,以便于说明。
|
||||
|
||||
首先假设已知从上一层回传给批量归一化层的误差矩阵是:
|
||||
|
||||
$$\delta = \frac{dJ}{dZ},\delta_i = \frac{dJ}{dz_i} \tag{10}$$
|
||||
|
||||
#### 求批量归一化层参数梯度
|
||||
|
||||
则根据公式9,求$\gamma,\beta$的梯度:
|
||||
|
||||
$$\frac{dJ}{d\gamma} = \sum_{i=1}^m \frac{dJ}{dz_i}\frac{dz_i}{d\gamma}=\sum_{i=1}^m \delta_i \cdot n_i \tag{11}$$
|
||||
|
||||
$$\frac{dJ}{d\beta} = \sum_{i=1}^m \frac{dJ}{dz_i}\frac{dz_i}{d\beta}=\sum_{i=1}^m \delta_i \tag{12}$$
|
||||
|
||||
注意$\gamma$和$\beta$的形状与批大小无关,只与特征值数量有关,我们假设特征值数量为1,所以它们都是一个标量。在从计算图看,它们都与N,Z的全集相关,而不是某一个样本,因此会用求和方式计算。
|
||||
|
||||
#### 求批量归一化层的前传误差矩阵
|
||||
|
||||
下述所有乘法都是element-wise的矩阵点乘,不再特殊说明。
|
||||
|
||||
从正向公式中看,对z有贡献的数据链是:
|
||||
|
||||
- $z_i \leftarrow n_i \leftarrow x_i$
|
||||
- $z_i \leftarrow n_i \leftarrow \mu_B \leftarrow x_i$
|
||||
- $z_i \leftarrow n_i \leftarrow \sigma^2_B \leftarrow x_i$
|
||||
- $z_i \leftarrow n_i \leftarrow \sigma^2_B \leftarrow \mu_B \leftarrow x_i$
|
||||
|
||||
从公式8,9:
|
||||
|
||||
$$
|
||||
\frac{dJ}{dx_i} = \frac{dJ}{d n_i}\frac{d n_i}{dx_i} + \frac{dJ}{d \sigma^2_B}\frac{d \sigma^2_B}{dx_i} + \frac{dJ}{d \mu_B}\frac{d \mu_B}{dx_i} \tag{13}
|
||||
$$
|
||||
|
||||
公式13的右侧第一部分(与全连接层形式一样):
|
||||
|
||||
$$
|
||||
\frac{dJ}{d n_i}= \frac{dJ}{dz_i}\frac{dz_i}{dn_i} = \delta_i \cdot \gamma\tag{14}
|
||||
$$
|
||||
|
||||
上式等价于:
|
||||
|
||||
$$
|
||||
\frac{dJ}{d N}= \delta \cdot \gamma\tag{14}
|
||||
$$
|
||||
|
||||
公式14中,我们假设样本数为64,特征值数为10,则得到一个$64\times 10$的结果矩阵(因为$1\times 10$的矩阵会被广播为$64\times 10$的矩阵):
|
||||
|
||||
$$\delta^{(64 \times 10)} \odot \gamma^{(1 \times 10)}=R^{(64 \times 10)}$$
|
||||
|
||||
公式13的右侧第二部分,从公式8:
|
||||
$$
|
||||
\frac{d n_i}{dx_i}=\frac{1}{\sqrt{\sigma^2_B + \epsilon}} \tag{15}
|
||||
$$
|
||||
|
||||
公式13的右侧第三部分,从公式8(注意$\sigma^2_B$是个标量,而且与X,N的全集相关,要用求和方式):
|
||||
|
||||
$$
|
||||
\begin{aligned}
|
||||
\frac{dJ}{d \sigma^2_B} &= \sum_{i=1}^m \frac{dJ}{d n_i}\frac{d n_i}{d \sigma^2_B}
|
||||
\\
|
||||
&= -\frac{1}{2}(\sigma^2_B + \epsilon)^{-3/2}\sum_{i=1}^m \frac{dJ}{d n_i} \cdot (x_i-\mu_B)
|
||||
\end{aligned}
|
||||
\tag{16}
|
||||
$$
|
||||
|
||||
公式13的右侧第四部分,从公式7:
|
||||
$$
|
||||
\frac{d \sigma^2_B}{dx_i} = \frac{2(x_i - \mu_B)}{m} \tag{17}
|
||||
$$
|
||||
|
||||
公式13的右侧第五部分,从公式7,8:
|
||||
|
||||
$$
|
||||
\frac{dJ}{d \mu_B}=\frac{dJ}{d n_i}\frac{d n_i}{d \mu_B} + \frac{dJ}{d\sigma^2_B}\frac{d \sigma^2_B}{d \mu_B} \tag{18}
|
||||
$$
|
||||
|
||||
公式18的右侧第二部分,根据公式8:
|
||||
|
||||
$$
|
||||
\frac{d n_i}{d \mu_B}=\frac{-1}{\sqrt{\sigma^2_B + \epsilon}} \tag{19}
|
||||
$$
|
||||
|
||||
公式18的右侧第四部分,根据公式7($\sigma^2_B和\mu_B$与全体$x_i$相关,所以要用求和):
|
||||
|
||||
$$
|
||||
\frac{d \sigma^2_B}{d \mu_B}=-\frac{2}{m}\sum_{i=1}^m (x_i- \mu_B) \tag{20}
|
||||
$$
|
||||
|
||||
所以公式18是:
|
||||
|
||||
$$
|
||||
\frac{dJ}{d \mu_B}=-\frac{\delta \cdot \gamma}{\sqrt{\sigma^2_B + \epsilon}} - \frac{2}{m}\frac{dJ}{d \sigma^2_B}\sum_{i=1}^m (x_i- \mu_B) \tag{18}
|
||||
$$
|
||||
|
||||
公式13的右侧第六部分,从公式6:
|
||||
|
||||
$$
|
||||
\frac{d \mu_B}{dx_i} = \frac{1}{m} \tag{21}
|
||||
$$
|
||||
|
||||
所以,公式13最后是这样的:
|
||||
|
||||
$$
|
||||
\frac{dJ}{dx_i} = \frac{\delta \cdot \gamma}{\sqrt{\sigma^2_B + \epsilon}} + \frac{dJ}{d\sigma^2_B} \cdot \frac{2(x_i - \mu_B)}{m} + \frac{dJ}{d\mu_B} \cdot \frac{1}{m} \tag{13}
|
||||
$$
|
||||
|
||||
### 15.6.2 代码实现
|
||||
|
||||
#### 初始化类
|
||||
|
||||
```Python
|
||||
class BnLayer(CLayer):
|
||||
def __init__(self, input_size, momentum=0.9):
|
||||
self.gamma = np.ones((1, input_size))
|
||||
self.beta = np.zeros((1, input_size))
|
||||
self.eps = 1e-5
|
||||
self.input_size = input_size
|
||||
self.output_size = input_size
|
||||
self.momentum = momentum
|
||||
self.running_mean = np.zeros((1,input_size))
|
||||
self.running_var = np.zeros((1,input_size))
|
||||
```
|
||||
后面三个变量,`momentum`、`running_mean`、`running_var`,是为了计算/记录历史方差均差的。
|
||||
|
||||
#### 前向计算
|
||||
|
||||
```Python
|
||||
def forward(self, input, train=True):
|
||||
......
|
||||
```
|
||||
前向计算完全按照上一节中的公式6到公式9实现。要注意在训练/测试阶段的不同算法,用train是否为True来做分支判断。
|
||||
|
||||
#### 反向传播
|
||||
|
||||
```Python
|
||||
def backward(self, delta_in, flag):
|
||||
......
|
||||
```
|
||||
|
||||
`d_norm_x`需要多次使用,所以先计算出来备用,以增加代码性能。
|
||||
|
||||
公式16中有一个$(\sigma^2_B + \epsilon)^{-3/2}$,在前向计算中,我们令:
|
||||
```Python
|
||||
self.var = np.mean(self.x_mu**2, axis=0, keepdims=True) + self.eps
|
||||
self.std = np.sqrt(self.var)
|
||||
```
|
||||
则:
|
||||
|
||||
$$self.var \times self.std = self.var \times self.var^{0.5}=self.var^{(3/2)}$$
|
||||
|
||||
放在分母中就是(-3/2)次方了。
|
||||
|
||||
另外代码中有很多`np.sum(..., axis=0, keepdims=True)`,这个和全连接层中的多样本计算一个道理,都是按样本数求和,并保持维度,便于后面的矩阵运算。
|
||||
|
||||
#### 更新参数
|
||||
```Python
|
||||
def update(self, learning_rate=0.1):
|
||||
self.gamma = self.gamma - self.d_gamma * learning_rate
|
||||
self.beta = self.beta - self.d_beta * learning_rate
|
||||
```
|
||||
更新$\gamma$和$\beta$时,我们使用0.1作为学习率。在初始化代码中,并没有给批量归一化层指定学习率,如果有需求的话,读者可以自行添加这部分逻辑。
|
||||
|
||||
### 15.6.3 批量归一化层的实际应用
|
||||
|
||||
首先回忆一下第14.6节中的MNIST的图片分类网络,当时的模型如图15-15所示。
|
||||
|
||||
<img src="./img/14/mnist_net.png" />
|
||||
|
||||
图15-15 第14.6节中MNIST图片分类网络
|
||||
|
||||
当时用了6个epoch(5763个Iteration),达到了0.12的预计loss值而停止训练。我们看看使用批量归一化后的样子,如图15-16所示。
|
||||
|
||||
<img src="./img/15/bn_mnist.png" />
|
||||
|
||||
图15-16 使用批量归一化后的MNIST图片分类网络
|
||||
|
||||
在全连接层和激活函数之间,加入一个批量归一化层,最后的分类函数Softmax前面不能加批量归一化。
|
||||
|
||||
#### 主程序代码
|
||||
|
||||
```Python
|
||||
if __name__ == '__main__':
|
||||
......
|
||||
params = HyperParameters_4_1(
|
||||
learning_rate, max_epoch, batch_size,
|
||||
net_type=NetType.MultipleClassifier,
|
||||
init_method=InitialMethod.MSRA,
|
||||
stopper=Stopper(StopCondition.StopLoss, 0.12))
|
||||
|
||||
net = NeuralNet_4_1(params, "MNIST")
|
||||
|
||||
fc1 = FcLayer_1_1(num_input, num_hidden1, params)
|
||||
net.add_layer(fc1, "fc1")
|
||||
bn1 = BnLayer(num_hidden1)
|
||||
net.add_layer(bn1, "bn1")
|
||||
r1 = ActivationLayer(Relu())
|
||||
net.add_layer(r1, "r1")
|
||||
......
|
||||
```
|
||||
|
||||
前后都省略了一些代码,注意上面代码片段中的bn1,就是应用了批量归一化层。
|
||||
|
||||
#### 运行结果
|
||||
|
||||
为了比较,我们使用与14.6中完全一致的参数设置来训练这个有批量归一化的模型,得到如图15-17所示的结果。
|
||||
|
||||
<img src="./img/15/bn_mnist_loss.png" />
|
||||
|
||||
图15-17 使用批量归一化后的MNIST图片分类网络训练结果
|
||||
|
||||
打印输出的最后几行如下:
|
||||
|
||||
```
|
||||
......
|
||||
epoch=4, total_iteration=4267
|
||||
loss_train=0.079916, accuracy_train=0.968750
|
||||
loss_valid=0.117291, accuracy_valid=0.967667
|
||||
time used: 19.44783306121826
|
||||
save parameters
|
||||
testing...
|
||||
0.9663
|
||||
```
|
||||
|
||||
列表15-12比较一下使用批量归一化前后的区别。
|
||||
|
||||
表15-12 批量归一化的作用
|
||||
|
||||
||不使用批量归一化|使用批量归一化|
|
||||
|---|---|---|
|
||||
|停止条件|loss < 0.12|loss < 0.12|
|
||||
|训练次数|6个epoch(5763次迭代)|4个epoch(4267次迭代)|
|
||||
|花费时间|17秒|19秒|
|
||||
|准确率|96.97%|96.63%|
|
||||
|
||||
使用批量归一化后,迭代速度提升,但是花费时间多了2秒,这是因为批量归一化的正向和反向计算过程还是比较复杂的,需要花费一些时间,但是批量归一化确实可以帮助网络快速收敛。如果使用GPU的话,花费时间上的差异应该可以忽略。
|
||||
|
||||
在准确率上的差异可以忽略,由于样本误差问题和随机初始化参数的差异,会造成最后的训练结果有细微差别。
|
||||
|
||||
### 代码位置
|
||||
|
||||
|
|
|
@ -3,118 +3,18 @@
|
|||
|
||||
## 16.1 偏差与方差
|
||||
|
||||
(do be add more...)
|
||||
以下为本小节目录,详情请参阅《智能之门》正版图书,高等教育出版社。
|
||||
|
||||
### 16.1.1 直观的解释
|
||||
|
||||
先用一个直观的例子来理解偏差和方差。比如打靶,如图16-9所示。
|
||||
|
||||
<img src="./img/16/variance_bias.png" width="600" ch="500" />
|
||||
|
||||
图16-9 打靶中的偏差和方差
|
||||
|
||||
总结一下,不同偏差和方差反映的射手的特点如表16-1所示。
|
||||
|
||||
表16-1 不同偏差和方差的射手特点
|
||||
|
||||
||低偏差|高偏差|
|
||||
|---|---|---|
|
||||
|低方差|射手很稳,枪的准星也很准。|射手很稳,但是枪的准星有问题,所有子弹都固定地偏向一侧。|
|
||||
|高方差|射手不太稳,但枪的准星没问题,虽然弹着点分布很散,但没有整体偏移。|射手不稳,而且枪的准星也有问题,弹着点分布很散且有规律地偏向一侧。|
|
||||
|
||||
### 16.1.2 神经网络训练的例子
|
||||
|
||||
我们在前面讲过数据集的使用,包括训练集、验证集、测试集。在训练过程中,我们要不断监测训练集和验证集在当前模型上的误差,和上面的打靶的例子一样,有可能产生四种情况,如表16-2所示。
|
||||
|
||||
表16-2 不同偏差和方差反映的四种情况
|
||||
|
||||
|情况|训练集误差A|验证集误差B|偏差|方差|说明|
|
||||
|---|---|---|---|---|---|
|
||||
|情况1|1.5%|1.7%|低偏差|低方差|A和B都很好,适度拟合|
|
||||
|情况2|12.3%|11.4%|高偏差|低方差|A和B都很不好,欠拟合|
|
||||
|情况3|1.2%|13.1%|低偏差|高方差|A很好,但B不好,过拟合|
|
||||
|情况4|12.3%|21.5%|高偏差|高方差|A不好,B更不好,欠拟合|
|
||||
|
||||
在本例中,偏差衡量训练集误差,方差衡量训练集误差和验证集误差的比值。
|
||||
|
||||
上述四种情况的应对措施:
|
||||
|
||||
- 情况1
|
||||
|
||||
效果很好,可以考虑进一步降低误差值,提高准确度。
|
||||
|
||||
- 情况2
|
||||
|
||||
训练集和验证集同时出现较大的误差,有可能是:迭代次数不够、数据不好、网络设计不好,需要继续训练,观察误差变化情况。
|
||||
|
||||
- 情况3
|
||||
|
||||
训练集的误差已经很低了,但验证集误差很高,说明过拟合了,即训练集中的某些特殊样本影响了网络参数,但类似的样本在验证集中并没有出现
|
||||
|
||||
- 情况4
|
||||
|
||||
两者误差都很大,目前还看不出来是什么问题,需要继续训练
|
||||
|
||||
### 16.1.3 偏差-方差分解
|
||||
|
||||
除了用上面的试验来估计泛化误差外,我们还希望在理论上分析其必然性,这就是偏差-方差分解的作用,bias-variance decomposition。表16-3是本章中使用的符号的含义,后续在推导公式的时候会用到。
|
||||
|
||||
表16-3 符号含义
|
||||
|
||||
|符号|含义|
|
||||
|---|---|
|
||||
|$x$|测试样本|
|
||||
|$D$|数据集|
|
||||
|$y$|x的真实标记|
|
||||
|$y_D$|x在数据集中标记(可能有误差)|
|
||||
|$f$|从数据集D学习的模型|
|
||||
|$f_{x;D}$|从数据集D学习的模型对x的预测输出|
|
||||
|$f_x$|模型f对x的期望预测输出|
|
||||
|
||||
学习算法期望的预测:
|
||||
$$f_x=E[f_{x;D}] \tag{1}$$
|
||||
不同的训练集/验证集产生的预测方差:
|
||||
$$var(x)=E[(f_{x;D}-f_x)^2] \tag{2}$$
|
||||
噪声:
|
||||
$$\epsilon^2=E[(y_D-y)^2] \tag{3}$$
|
||||
期望输出与真实标记的偏差:
|
||||
$$bias^2(x)=(f_x-y)^2 \tag{4}$$
|
||||
算法的期望泛化误差:
|
||||
|
||||
$$
|
||||
\begin{aligned}
|
||||
E(f;D)&=E[(f_{x;D}-y_D)^2]=E[(f_{x;D}-f_x+f_x-y_D)^2] \\\\
|
||||
&=E[(f_{x;D}-f_x)^2]+E[(f_x-y_D)^2]+E[2(f_{x;D}-f_x)(f_x-y_D)]=E[(f_{x;D}-f_x)^2]+E[(f_x-y_D)^2] \\\\
|
||||
&=E[(f_{x;D}-f_x)^2]+E[(f_x-y+y-y_D)^2]=E[(f_{x;D}-f_x)^2]+E[(f_x-y)^2]+E(y-y_D)^2]+E[2(f_x-y)(y-y_D)] \\\\
|
||||
&=E[(f_{x;D}-f_x)^2]+(f_x-y)^2+E[(y-y_D)^2]=var(x) + bias^2(x) + \epsilon^2
|
||||
\end{aligned}
|
||||
$$
|
||||
|
||||
所以,各个项的含义是:
|
||||
|
||||
- 偏差:度量了学习算法的期望与真实结果的偏离程度,即学习算法的拟合能力。
|
||||
- 方差:训练集与验证集的差异造成的模型表现的差异。
|
||||
- 噪声:当前数据集上任何算法所能到达的泛化误差的下线,即学习问题本身的难度。
|
||||
|
||||
想当然地,我们希望偏差与方差越小越好,但实际并非如此。一般来说,偏差与方差是有冲突的,称为偏差-方差窘境 (bias-variance dilemma)。
|
||||
|
||||
- 给定一个学习任务,在训练初期,由于训练不足,网络的拟合能力不够强,偏差比较大,也是由于拟合能力不强,数据集的特征也无法使网络产生显著变化,也就是欠拟合的情况。
|
||||
- 随着训练程度的加深,网络的拟合能力逐渐增强,训练数据的特征也能够渐渐被网络学到。
|
||||
- 充分训练后,网络的拟合能力已非常强,训练数据的微小特征都会导致网络发生显著变化,当训练数据自身的、非全局的特征被网络学到了,则将发生过拟合。
|
||||
|
||||
<img src="./img/16/error.png" width="600" ch="500" />
|
||||
|
||||
图16-10 训练过程中的偏差和方差变化
|
||||
|
||||
在图16-10中,随着训练程度的增加,偏差(点线)一路下降,但是方差(虚线)一路上升,整体误差(实线,偏差+方差+噪音误差)呈U形,最佳平衡点就是U形的最低点。
|
||||
|
||||
### 16.1.4 没有免费午餐定理
|
||||
|
||||
没有免费午餐定理(No Free Lunch Theorem,NFL)是由Wolpert和Macerday在最优化理论中提出的。没有免费午餐定理证明:对于基于迭代的最优化算法,不存在某种算法对所有问题(有限的搜索空间内)都有效。如果一个算法对某些问题有效,那么它一定在另外一些问题上比纯随机搜索算法更差。
|
||||
|
||||
还可以理解为在所有可能的数据生成分布上平均之后,每一个分类算法在未事先观测的点上都有相同的错误率。也就是说,不能脱离具体问题来谈论算法的优劣,任何算法都有局限性。必须要“具体问题具体分析”。
|
||||
|
||||
没有免费午餐定理对于机器学习算法也同样适用。不存在一种机器学习算 法适合于任何领域或任务。如果有人宣称自己的模型在所有问题上都好于其他模型,那么他肯定是在吹牛。
|
||||
|
||||
### 参考资料
|
||||
|
||||
|
|
|
@ -3,228 +3,20 @@
|
|||
|
||||
## 16.2 L2正则
|
||||
|
||||
以下为本小节目录,详情请参阅《智能之门》正版图书,高等教育出版社。
|
||||
|
||||
### 16.2.1 朴素的想法
|
||||
|
||||
从过拟合的现象分析,是因为神经网络的权重矩阵参数过度地学习,即针对训练集,其损失函数值已经逼近了最小值。我们用熟悉的等高线图来解释,如图16-11所示。
|
||||
|
||||
<img src="./img/16/regular0.png" />
|
||||
|
||||
图16-11 损失函数值的等高线图
|
||||
|
||||
假设只有两个参数需要学习,那么这两个参数的损失函数就构成了上面的等高线图。由于样本数据量比较小(这是造成过拟合的原因之一),所以神经网络在训练过程中沿着箭头方向不断向最优解靠近,最终达到了过拟合的状态。也就是说在这个等高线图中的最优解,实际是针对有限的样本数据的最优解,而不是针对这个特点问题的最优解。
|
||||
|
||||
由此会产生一个朴素的想法:如果我们以某个处于中间位置等高线上(比如那条红色的等高线)为目标的话,是不是就可以得到比较好的效果呢?如何科学地找到这条等高线呢?
|
||||
|
||||
### 16.2.2 基本数学知识
|
||||
|
||||
#### 范数
|
||||
|
||||
回忆一下范数的基本概念:
|
||||
|
||||
$$L_p = \lVert x \rVert_p = ({\sum^n_{i=1}\lvert x_i \rvert^p})^{1/p} \tag{1}$$
|
||||
|
||||
范数包含向量范数和矩阵范数,我们只关心向量范数。我们用具体的数值来理解范数。假设有一个向量a:
|
||||
|
||||
$$a=[1,-2,0,-4]$$
|
||||
|
||||
$$L_0=3 \tag{非0元素数}$$
|
||||
$$L_1 = \sum^3_{i=0}\lvert x_i \rvert = 1+2+0+4=7 \tag{绝对值求和}$$
|
||||
$$L_2 = \sqrt[2]{\sum^3_{i=0}\lvert x_i \rvert^2} =\sqrt[2]{21}=4.5826 \tag{平方和求方根}$$
|
||||
$$L_{\infty}=4 \tag{最大值的绝对值}$$
|
||||
|
||||
注意p可以是小数,比如0.5:
|
||||
|
||||
$$L_{0.5}=19.7052$$
|
||||
|
||||
一个经典的关于P范数的变化如图16-12所示。
|
||||
|
||||
<img src="./img/16/norm.png" />
|
||||
|
||||
图16-12 P范数变化图
|
||||
|
||||
我们只关心L1和L2范数:
|
||||
|
||||
- L1范数是个菱形体,在平面上是一个菱形
|
||||
- L2范数是个球体,在平面上是一个圆
|
||||
|
||||
#### 高斯分布
|
||||
|
||||
$$
|
||||
f(x)=\frac{1}{\sigma\sqrt{2 \pi}} \exp{- \frac{(x-\mu)^2}{2\sigma^2}} \tag{2}
|
||||
$$
|
||||
|
||||
请参考15.2一节。
|
||||
|
||||
### 16.2.3 L2正则化
|
||||
|
||||
假设:
|
||||
|
||||
- W参数服从高斯分布,即:$w_j \sim N(0,\tau^2)$
|
||||
- Y服从高斯分布,即:$y_i \sim N(w^Tx_i,\sigma^2)$
|
||||
|
||||
贝叶斯最大后验估计:
|
||||
|
||||
$$
|
||||
\arg\max_wL(w) = \ln \prod_i^n \frac{1}{\sigma\sqrt{2 \pi}}\exp(-(\frac{y_i-w^Tx_i}{\sigma})^2/2) \cdot \prod_j^m{\frac{1}{\tau\sqrt{2\pi}}\exp(-(\frac{w_j}{\tau})^2/2)}
|
||||
$$
|
||||
|
||||
$$
|
||||
=-\frac{1}{2\sigma^2}\sum_i^n(y_i-w^Tx_i)^2-\frac{1}{2\tau^2}\sum_j^m{w_j^2}-n\ln\sigma\sqrt{2\pi}-m\ln \tau\sqrt{2\pi} \tag{3}
|
||||
$$
|
||||
|
||||
因为$\sigma,b,n,\pi,m$等都是常数,所以损失函数$J(w)$的最小值可以简化为:
|
||||
|
||||
$$
|
||||
\arg\min_wJ(w) = \sum_i^n(y_i-w^Tx_i)^2+\lambda\sum_j^m{w_j^2} \tag{4}
|
||||
$$
|
||||
|
||||
看公式4,相当于是线性回归的均方差损失函数,再加上一个正则项(也称为惩罚项),共同构成损失函数。如果想求这个函数的最小值,则需要两者协调,并不是说分别求其最小值就能实现整体最小,因为它们具有共同的W项,当W比较大时,第一项比较小,第二项比较大,或者正好相反。所以它们是矛盾组合体。
|
||||
|
||||
为了简化问题便于理解,我们用两个参数$w_1,w_2$举例。对于公式4的第一项,我们用前面学习过损失函数的等高线图来解释。对于第二项,形式应该是一个圆形,因为圆的方程是$r^2=x^2+y^2$。所以,结合两者,我们可以得到图16-13。
|
||||
|
||||
<img src="./img/16/regular2.png" ch="500" />
|
||||
|
||||
图16-13 L2正则区与损失函数等高线示意图
|
||||
|
||||
黄色的圆形,就是正则项所处的区域。这个区域的大小,是由参数$\lambda$所控制的,该值越大,黄色圆形区域越小,对w的惩罚力度越大(距离椭圆中心越远)。比如图16-13中分别标出了该值为0.7、0.8、0.9的情况。
|
||||
|
||||
还以图16-13为例,当$\lambda$为0.7时,L2正则区为图中所示最大的黄色区域,此区域与损失函数等高线图的交点有多个,比如图中的红、绿、蓝三个点,但由于红点距离椭圆中心最近,所以最后求得的权重值应该在红点的位置坐标上$(w_1,w_2)$。
|
||||
|
||||
在回归里面,把具有L2项的回归叫“岭回归”(Ridge Regression),也叫它“权值衰减”(weight decay)。 weight decay还有一个好处,它使得目标函数变为凸函数,梯度下降法和L-BFGS都能收敛到全局最优解。
|
||||
|
||||
L2范数是指向量各元素的平方和然后求平方根。我们让L2范数的规则项最小,可以使得W的每个元素都很小,都接近于0,因为一般认为参数值小的模型比较简单,能适应不同的数据集,也在一定程度上避免了过拟合现象。可以设想一下对于一个线性回归方程,若参数很大,那么只要数据偏移一点点,就会对结果造成很大的影响;但如果参数足够小,数据偏移得多一点也不会对结果造成什么影响,专业一点的说法是“抗扰动能力强”。
|
||||
|
||||
#### 关于bias偏置项的正则
|
||||
|
||||
上面的L2正则化没有约束偏置(biases)项。当然,通过修改正则化过程来正则化偏置会很容易,但根据经验,这样做往往不能较明显地改变结果,所以是否正则化偏置项仅仅是一个习惯问题。
|
||||
|
||||
值得注意的是,有一个较大的bias并不会使得神经元对它的输入像有大权重那样敏感,所以不用担心较大的偏置会使我们的网络学习到训练数据中的噪声。同时,允许大的偏置使我们的网络在性能上更为灵活,特别是较大的偏置使得神经元更容易饱和,这通常是我们期望的。由于这些原因,通常不对偏置做正则化。
|
||||
|
||||
### 16.2.4 损失函数的变化
|
||||
|
||||
假设是均方差损失函数:
|
||||
|
||||
$$J(w,b)=\frac{1}{2m}\sum_{i=1}^m (z_i-y_i)^2 + \frac{\lambda}{2m}\sum_{j=1}^n{w_j^2} \tag{5}$$
|
||||
|
||||
如果是交叉熵损失函数:
|
||||
|
||||
$$J(w,b)= -\frac{1}{m} \sum_{i=1}^m [y_i \ln a_i + (1-y_i) \ln (1-a_i)]+ \frac{\lambda}{2m}\sum_{j=1}^n{w_j^2} \tag{6}$$
|
||||
|
||||
在`NeuralNet.py`中的代码片段如下,计算公式5或公式6的第二项:
|
||||
|
||||
```Python
|
||||
for i in range(self.layer_count-1,-1,-1):
|
||||
layer = self.layer_list[i]
|
||||
if isinstance(layer, FcLayer):
|
||||
if regularName == RegularMethod.L2:
|
||||
regular_cost += np.sum(np.square(layer.weights.W))
|
||||
|
||||
return regular_cost * self.params.lambd
|
||||
```
|
||||
如果是FC层,则取出W值的平方,再求和,最后乘以$\lambda$系数返回。
|
||||
|
||||
在计算Loss值时,用上面函数的返回值再除以样本数m,即下面代码中的`train_y.shape[0]`,附加到原始的loss值之后即可。下述代码就是对公式5或6的实现。
|
||||
|
||||
```Python
|
||||
loss_train = self.lossFunc.CheckLoss(train_y, self.output)
|
||||
loss_train += regular_cost / train_y.shape[0]
|
||||
```
|
||||
|
||||
### 16.2.5 反向传播的变化
|
||||
|
||||
由于正则项是在损失函数中,在正向计算中,并不涉及到它,所以正向计算公式不用变。但是在反向传播过程中,需要重新推导一下公式。
|
||||
|
||||
假设有一个两层的回归神经网络,其前向计算如下:
|
||||
|
||||
$$
|
||||
Z1 = W1 \cdot X + B1 \tag{5}
|
||||
$$
|
||||
$$
|
||||
A1 = Sigmoid(Z1) \tag{6}
|
||||
$$
|
||||
$$
|
||||
Z2 = W2 \cdot A1 + B2 \tag{7}
|
||||
$$
|
||||
$$
|
||||
J(w,b)=\frac{1}{2m}[\sum_{i=1}^m (z_i-y_i)^2 + \lambda\sum_{j=1}^n{w_j^2}] \tag{8}
|
||||
$$
|
||||
从公式8求Z2的误差矩阵:
|
||||
$$
|
||||
dZ2 = \frac{dJ}{dZ2}=Z2-Y
|
||||
$$
|
||||
从公式8求W2的误差矩阵,因为有正则项存在,所以需要附加一项:
|
||||
$$
|
||||
\begin{aligned}
|
||||
\frac{dJ}{dW2}&=\frac{dJ}{dZ2}\frac{dZ2}{dW2}+\frac{dJ}{dW2}
|
||||
\\
|
||||
&=(Z2-Y)\cdot A1^T+\lambda \odot W2
|
||||
\end{aligned}
|
||||
\tag{9}
|
||||
$$
|
||||
|
||||
公式8是W1,W2的总和,公式9对dJ/dW2求导时,由于是$W1^2+W2^2$的关系,所以W1对W2求导的结果是0,所以公式9最后只剩下W2了。
|
||||
|
||||
B不受正则项的影响:
|
||||
|
||||
$$dB2=dZ2 \tag{10}$$
|
||||
|
||||
再继续反向传播到第一层网络:
|
||||
|
||||
$$dZ1 = W2^T \times dZ2 \odot A1 \odot (1-A1) \tag{11}$$
|
||||
|
||||
$$dW1= dZ1 \cdot X^T + \lambda \odot W1 \tag{12}$$
|
||||
|
||||
$$dB1= dZ1 \tag{13}$$
|
||||
|
||||
从上面的公式中可以看到,正则项在方向传播过程中,唯一影响的就是求W的梯度时,要增加一个$\lambda \odot W$,所以,我们可以修改`FullConnectionLayer.py`中的反向传播函数如下:
|
||||
|
||||
```Python
|
||||
def backward(self, delta_in, idx):
|
||||
dZ = delta_in
|
||||
m = self.x.shape[1]
|
||||
if self.regular == RegularMethod.L2:
|
||||
self.weights.dW = (np.dot(dZ, self.x.T) + self.lambd * self.weights.W) / m
|
||||
else:
|
||||
self.weights.dW = np.dot(dZ, self.x.T) / m
|
||||
# end if
|
||||
self.weights.dB = np.sum(dZ, axis=1, keepdims=True) / m
|
||||
|
||||
delta_out = np.dot(self.weights.W.T, dZ)
|
||||
|
||||
if len(self.input_shape) > 2:
|
||||
return delta_out.reshape(self.input_shape)
|
||||
else:
|
||||
return delta_out
|
||||
```
|
||||
当`regular == RegularMethod.L2`时,走一个特殊分支,完成正则项的惩罚机制。
|
||||
|
||||
### 16.2.6 运行结果
|
||||
|
||||
下面是主程序的运行代码:
|
||||
|
||||
```Python
|
||||
from Level0_OverFitNet import *
|
||||
|
||||
if __name__ == '__main__':
|
||||
dr = LoadData()
|
||||
hp, num_hidden = SetParameters()
|
||||
hp.regular_name = RegularMethod.L2
|
||||
hp.regular_value = 0.01
|
||||
net = Model(dr, 1, num_hidden, 1, hp)
|
||||
ShowResult(net, dr, hp.toString())
|
||||
```
|
||||
|
||||
运行后,将训练过程中的损失和准确率可视化出来,并将拟合后的曲线与训练数据做比较,如图16-14和16-15所示。
|
||||
|
||||
<img src="./img/16/L2_sin_loss.png" />
|
||||
|
||||
图16-14 训练过程中损失函数值和准确率的变化曲线
|
||||
|
||||
<img src="./img/16/L2_sin_result.png" ch="500" />
|
||||
|
||||
图16-15 拟合后的曲线与训练数据的分布图
|
||||
|
||||
|
||||
### 代码位置
|
||||
|
||||
ch16, Level2
|
||||
|
|
|
@ -3,261 +3,22 @@
|
|||
|
||||
## 16.3 L1正则
|
||||
|
||||
以下为本小节目录,详情请参阅《智能之门》正版图书,高等教育出版社。
|
||||
|
||||
### 16.3.1 另一个朴素的想法
|
||||
|
||||
我们把熟悉的等高线图拿出来再看一眼,如图16-16所示。
|
||||
|
||||
<img src="./img/16/regular0.png" />
|
||||
|
||||
图16-16 损失函数值的等高线图
|
||||
|
||||
假设只有两个参数需要学习,那么这两个参数的损失函数就构成了上面的等高线图。
|
||||
|
||||
在L2正则中,我们想办法让W的值都变得比较小,这样就不会对特征敏感。但是也会杀敌一千,自损八百,连有用特征一起被忽视掉了。那么换个思路,能不能让神经网络自动选取有用特征,忽视无用特征呢?也就是让有用特征的权重比较大,让无用特征的权重比较小,甚至为0。
|
||||
|
||||
用上面的图举例,公式为:
|
||||
|
||||
$$z=x_1 \cdot w_1 + x_2 \cdot w_2 + b$$
|
||||
|
||||
假设$x_1$是无用特征,想办法让$w_1$变得很小或者是0,就会得到比较满意的模型。这种想法在只有两个特征值时不明显,甚至不正确,但是当特征值有很多时,比如MNIST数据中的784个特征,肯定有些是非常重要的特征,有些是没什么用的特征。
|
||||
|
||||
### 16.3.2 基本数学知识
|
||||
|
||||
#### 拉普拉斯分布
|
||||
|
||||
$$
|
||||
\begin{aligned}
|
||||
f(x)&=\frac{1}{2b}\exp(-\frac{|x-\mu|}{b})\\\\
|
||||
&= \frac{1}{2b} \begin{cases} \exp(\frac{x-\mu}{b}), & x \lt \mu \\\\ \exp(\frac{\mu-x}{b}), & x \gt \mu \end{cases}
|
||||
\end{aligned}
|
||||
$$
|
||||
|
||||
|
||||
#### L0范数与L1范数
|
||||
|
||||
L0范数是指向量中非0的元素的个数。如果我们用L0范数来规则化一个参数矩阵W的话,就是希望W的大部分元素都是0,即让参数W是稀疏的。
|
||||
|
||||
L1范数是指向量中各个元素绝对值之和,也叫“稀疏规则算子”(Lasso regularization)。为什么L1范数会使权值稀疏?有人可能会这样给你回答“它是L0范数的最优凸近似”。实际上,还存在一个更美的回答:任何的规则化算子,如果他在$w_i=0$的地方不可微,并且可以分解为一个“求和”的形式,那么这个规则化算子就可以实现稀疏。w的L1范数是绝对值,所以$|w|$在$w=0$处是不可微。
|
||||
|
||||
为什么L0和L1都可以实现稀疏,但常用的为L1?一是因为L0范数很难优化求解,二是L1范数是L0范数的最优凸近似,而且它比L0范数要容易优化求解。所以大家才把目光转于L1范数。
|
||||
|
||||
综上,L1范数和L0范数可以实现稀疏,L1因具有比L0更好的优化求解特性而被广泛应用。
|
||||
|
||||
### 16.3.3 L1正则化
|
||||
|
||||
假设:
|
||||
|
||||
- W参数服从拉普拉斯分布,即$w_j \sim Laplace(0,b)$
|
||||
- Y服从高斯分布,即$y_i \sim N(w^Tx_i,\sigma^2)$
|
||||
|
||||
贝叶斯最大后验估计:
|
||||
$$
|
||||
\begin{aligned}
|
||||
\arg\max_wL(w) = &\ln \prod_i^n \frac{1}{\sigma\sqrt{2 \pi}}\exp(-\frac{1}{2}(\frac{y_i-w^Tx_i}{\sigma})^2)
|
||||
\cdot \prod_j^m{\frac{1}{2b}\exp(-\frac{\lvert w_j \rvert}{b})}
|
||||
\\\\
|
||||
=&-\frac{1}{2\sigma^2}\sum_i^n(y_i-w^Tx_i)^2-\frac{1}{2b}\sum_j^m{\lvert w_j \rvert}
|
||||
-n\ln\sigma\sqrt{2\pi}-m\ln b\sqrt{2\pi}
|
||||
\end{aligned}
|
||||
\tag{1}
|
||||
$$
|
||||
|
||||
因为$\sigma,b,n,\pi,m$等都是常数,所以损失函数$J(w)$的最小值可以简化为:
|
||||
|
||||
$$
|
||||
\arg\min_wJ(w) = \sum_i^n(y_i-w^Tx_i)^2+\lambda\sum_j^m{\lvert w_j \rvert} \tag{2}
|
||||
$$
|
||||
|
||||
我们仍以两个参数为例,公式2的后半部分的正则形式为:
|
||||
|
||||
$$L_1 = \lvert w_1 \rvert + \lvert w_2 \rvert \tag{3}$$
|
||||
|
||||
因为$w_1,w_2$有可能是正数或者负数,我们令$x=|w_1|,y=|w_2|,c=L_1$,则公式3可以拆成以下4个公式的组合:
|
||||
|
||||
$$
|
||||
y=-x+c \quad (当w_1 \gt 0, w_2 \gt 0时)
|
||||
$$
|
||||
$$
|
||||
y=\quad x+c \quad (当w_1 \lt 0, w_2 \gt 0时)
|
||||
$$
|
||||
$$
|
||||
y=\quad x-c \quad (当w_1 \gt 0, w_2 \lt 0时)
|
||||
$$
|
||||
$$
|
||||
y=-x-c \quad (当w_1 \lt 0, w_2 \lt 0时)
|
||||
$$
|
||||
|
||||
所以上述4个公式(4条直线)会组成一个二维平面上的一个菱形。
|
||||
|
||||
图16-17中三个菱形,是因为惩罚因子的数值不同而形成的,越大的话,菱形面积越小,惩罚越厉害。
|
||||
|
||||
<img src="./img/16/regular1.png" ch="500" />
|
||||
|
||||
图16-17 L1正则区与损失函数等高线示意图
|
||||
|
||||
以最大的那个菱形区域为例,它与损失函数等高线有多个交点,都可以作为此问题的解,但是其中红色顶点是损失函数值最小的,因此它是最优解。
|
||||
|
||||
图16-17中菱形的红色顶点的含义具有特殊性,即$W=[w2, 0]$,也就是w1的值为0。扩充到三维空间,菱形的6个顶点,上下的两个顶点是z值不为0,xy值为0;左右的两个顶点是x值不为0,yz值为0;前后的两个顶点是y值不为0,xz值为0。也就是说,如果xyz是三个权重值的话,那么顶点上只有一个权重值不为0,其它两个都是0。
|
||||
|
||||
高维空间,其顶点就是只有少数的参数有非零值,其它参数都为0。这就是所谓的稀疏解。可以这样理解,这个菱形像个刺猬,用它去触碰一个气球,一定是刺尖儿先扎到气球。上图中的三个菱形,都是顶点先接触到等高线。
|
||||
|
||||
在回归里面,把具有L1项的回归叫“Lasso Regression”(Tibshirani, 1995, Least Absolute Shrinkage and Selection Operator)。
|
||||
|
||||
### 16.3.4 损失函数的变化
|
||||
|
||||
假设我们以前使用的损失函数为$J_0$,则新的损失函数变成:
|
||||
|
||||
$$J = J_0 + \frac{\lambda}{m} \sum_i^m \lvert w_i \rvert$$
|
||||
|
||||
代码片段如下:
|
||||
|
||||
```Python
|
||||
regular_cost = 0
|
||||
for i in range(self.layer_count-1,-1,-1):
|
||||
layer = self.layer_list[i]
|
||||
if isinstance(layer, FcLayer):
|
||||
if regularName == RegularMethod.L1:
|
||||
regular_cost += np.sum(np.abs(layer.weights.W))
|
||||
elif regularName == RegularMethod.L2:
|
||||
regular_cost += np.sum(np.square(layer.weights.W))
|
||||
# end if
|
||||
# end for
|
||||
return regular_cost * self.params.lambd
|
||||
```
|
||||
|
||||
可以看到L1部分的代码,先求绝对值,再求和。那个分母上的m是在下一段代码中处理的,因为在上一段代码中,没有任何样本数量的信息。
|
||||
|
||||
```Python
|
||||
loss_train = self.lossFunc.CheckLoss(train_y, self.output)
|
||||
loss_train += regular_cost / train_y.shape[0]
|
||||
```
|
||||
`train_y.shape[0]`就是样本数量。
|
||||
|
||||
### 16.3.5 反向传播的变化
|
||||
|
||||
假设一个两层的神经网络,其前向过程是:
|
||||
|
||||
$$Z1=W1 \cdot X + B1$$
|
||||
$$A1 = Sigmoid(Z1)$$
|
||||
$$Z2=W2 \cdot A1 + B2$$
|
||||
$$J(w,b) = J_0 + \lambda (\lvert W1 \rvert+\lvert W2 \rvert)$$
|
||||
|
||||
则反向过程为:
|
||||
|
||||
$$
|
||||
\begin{aligned}
|
||||
dW2&=\frac{dJ}{dW2}=\frac{dJ}{dZ2}\frac{dZ2}{dW2}+\frac{dJ}{dW2} \\\\
|
||||
&=dZ2 \cdot A1^T+\lambda \odot sign(W2)
|
||||
\end{aligned}
|
||||
$$
|
||||
$$dW1= dZ1 \cdot X^T + \lambda \odot sign(W1) $$
|
||||
|
||||
从上面的公式中可以看到,正则项在方向传播过程中,唯一影响的就是求W的梯度时,要增加一个$\lambda \odot sign(W)$,sign是符号函数,返回该值的符号,即1或-1。所以,我们可以修改`FullConnectionLayer.py`中的反向传播函数如下:
|
||||
|
||||
```Python
|
||||
def backward(self, delta_in, idx):
|
||||
dZ = delta_in
|
||||
m = self.x.shape[1]
|
||||
if self.regular == RegularMethod.L2:
|
||||
self.weights.dW = (np.dot(dZ, self.x.T) + self.lambd * self.weights.W) / m
|
||||
elif self.regular == RegularMethod.L1:
|
||||
self.weights.dW = (np.dot(dZ, self.x.T) + self.lambd * np.sign(self.weights.W)) / m
|
||||
else:
|
||||
self.weights.dW = np.dot(dZ, self.x.T) / m
|
||||
# end if
|
||||
self.weights.dB = np.sum(dZ, axis=1, keepdims=True) / m
|
||||
......
|
||||
```
|
||||
符号函数的效果如下:
|
||||
```Python
|
||||
>>> a=np.array([1,-1,2,0])
|
||||
>>> np.sign(a)
|
||||
>>> array([ 1, -1, 1, 0])
|
||||
```
|
||||
当w为正数时,符号为正,值为1,相当于直接乘以w的值;当w为负数时,符号为负,值为-1,相当于乘以(-w)的值。最后的效果就是乘以w的绝对值。
|
||||
|
||||
### 16.3.6 运行结果
|
||||
|
||||
在主过程中,修改超参实例如下:
|
||||
```Python
|
||||
from Level0_OverFitNet import *
|
||||
|
||||
if __name__ == '__main__':
|
||||
dr = LoadData()
|
||||
hp, num_hidden = SetParameters()
|
||||
hp.regular_name = RegularMethod.L1
|
||||
hp.regular_value = 0.005
|
||||
net = Model(dr, 1, num_hidden, 1, hp)
|
||||
ShowResult(net, dr, hp.toString())
|
||||
```
|
||||
|
||||
设置L1正则方法,系数为0.005。
|
||||
|
||||
<img src="./img/16/L1_sin_loss.png" />
|
||||
|
||||
图16-18 训练过程中损失函数值和准确率的变化曲线
|
||||
|
||||
从图16-18上看,无论是损失函数值还是准确率,在训练集上都没有表现得那么夸张了,不会极高(到100%)或者极低(到0.001)。这说明过拟合的情况得到了抑制,而且准确率提高到了99.18%。还可以画出拟合后的曲线与训练数据的分布做对比,如图16-19所示。
|
||||
|
||||
<img src="./img/16/L1_sin_result.png" ch="500" />
|
||||
|
||||
图16-19 拟合后的曲线与训练数据的分布图
|
||||
|
||||
从输出结果分析:
|
||||
|
||||
1. 权重值的绝对值和等于391.26,远小于过拟合时的1719
|
||||
2. 较小的权重值(小于0.01)的数量为22935个,远大于过拟合时的2810个
|
||||
3. 趋近于0的权重值(小于0.0001)的数量为12384个,大于过拟合时的25个。
|
||||
|
||||
可以看到L1的模型权重非常稀疏(趋近于0的数量很多)。那么参数稀疏有什么好处呢?有两点:
|
||||
|
||||
1. 特征选择(Feature Selection):
|
||||
|
||||
大家对稀疏规则化趋之若鹜的一个关键原因在于它能实现特征的自动选择。一般来说,x的大部分元素(也就是特征)都是和最终的输出y没有关系或者不提供任何信息的,在最小化目标函数的时候考虑x这些额外的特征,虽然可以获得更小的训练误差,但在预测新的样本时,这些没用的信息反而会被考虑,从而干扰了对正确y的预测。稀疏规则化算子的引入就是为了完成特征自动选择的光荣使命,它会学习地去掉这些没有信息的特征,也就是把这些特征对应的权重置为0。
|
||||
|
||||
2. 可解释性(Interpretability):
|
||||
|
||||
另一个青睐于稀疏的理由是,模型更容易解释。例如患某种病的概率是y,然后我们收集到的数据x是1000维的,也就是我们需要寻找这1000种因素到底是怎么影响患上这种病的概率的。假设我们这个是个回归模型:$y=w_1x_1+w_2x_2+…+w_{1000}x_{1000}+b$(当然了,为了让y限定在$[0,1]$的范围,一般还得加个Logistic函数)。通过学习,如果最后学习到的w就只有很少的非零元素,例如只有5个非零的wi,那么我们就有理由相信,这些对应的特征在患病分析上面提供的信息是巨大的,决策性的。也就是说,患不患这种病只和这5个因素有关,那医生就好分析多了。但如果1000个$w_i$都非0,医生面对这1000种因素,无法采取针对性治疗。
|
||||
|
||||
|
||||
### 16.3.7 L1和L2的比较
|
||||
|
||||
表16-4展示了L1和L2两种正则方法的比较项目。
|
||||
|
||||
表16-4 L1和L2的比较
|
||||
|
||||
|比较项|无正则项|L2|L1|
|
||||
|---|---|---|---|
|
||||
|代价函数|$J(w,b)$|$J(w,b)+\lambda \Vert w \Vert^2_2$|$J(w,b)+\lambda \Vert w \Vert_1$|
|
||||
|梯度计算|$dw$|$dw+\lambda \cdot w/m$|$dw+\lambda \cdot sign(w)/m$|
|
||||
|准确率|0.961|0.982|0.987||
|
||||
|总参数数量|544|544|544|
|
||||
|小值参数数量(<1e-2)|7|204|524|
|
||||
|极小值参数数量(<1e-5)|0|196|492|
|
||||
|第1层参数Norm1|8.66|6.84|4.09|
|
||||
|第2层参数Norm1|104.26|34.44|6.38|
|
||||
|第3层参数Norm1|97.74|18.96|6.73|
|
||||
|第4层参数Norm1|9.03|4.22|4.41|
|
||||
|第1层参数Norm2|2.31|1.71|1.71|
|
||||
|第2层参数Norm2|6.81|2.15|2.23|
|
||||
|第3层参数Norm2|5.51|2.45|2.81|
|
||||
|第4层参数Norm2|2.78|2.13|2.59|
|
||||
|
||||
#### 第一范数值的比较
|
||||
|
||||
通过比较各层的权重值的第一范数值Norm1,可以看到L1正则化的值最小,因为L1正则的效果就是让权重参数矩阵稀疏化,以形成特征选择。用通俗的话讲就是权重值矩阵中很多为项0或者接近0,这把有用的特征提出来,无用特征的影响非常小甚至为0。
|
||||
|
||||
这一点从参数值小于`1e-4`的数量中也可以看出来,一共才有544个参数,L1达到了492个,90%的参数都是很小的数。
|
||||
|
||||
L2正则化的Norm1的值,比无正则项时也小很多,说明参数值普遍减小了。
|
||||
|
||||
#### 第二范数值的比较
|
||||
|
||||
比较各层的第二范数值Norm2,可以看到L2正则化的值最小,也就是说L2正则化的结果,是使得权重矩阵中的值普遍减小,拉向坐标原点。权重值变小,就会对特征不敏感,大部分特征都能起作用时。
|
||||
|
||||
这一点从参数值小于`1e-2`的数量中也可以看出来,有204个参数都小于`1e-2`,与没有正则项时的7个形成了鲜明对比。
|
||||
|
||||
为什么L2和L1的Norm2值相差无几呢?原因是虽然L1的权重矩阵值为0的居多,但是针对有些特征的权重值比较大,形成了“一枝独秀”的效果,所以Norm2的值并不会很小。而L2的权重矩阵值普遍较小,小于`1e-4`的个数比L1少很多,属于“百花齐放”的效果。
|
||||
|
||||
### 代码位置
|
||||
|
||||
|
|
|
@ -3,169 +3,18 @@
|
|||
|
||||
## 16.4 早停法 Early Stopping
|
||||
|
||||
以下为本小节目录,详情请参阅《智能之门》正版图书,高等教育出版社。
|
||||
|
||||
### 16.4.1 想法的由来
|
||||
|
||||
从图16-20来看,如果我们在第2500次迭代时就停止训练,就应该是验证集的红色曲线的最佳取值位置了,因为此时损失值最小,而准确率值最大。
|
||||
|
||||
<img src="./img/16/overfitting_sin_loss.png" />
|
||||
|
||||
图16-20 训练过程中损失函数值和准确率的变化曲线
|
||||
|
||||
这种做法很符合直观感受,因为准确率都不再提高了,损失值反而上升了,再继续训练也是无益的,只会浪费训练的时间。那么该做法的一个重点便是怎样才认为验证集不再提高了呢?并不是说准确率一降下来便认为不再提高了,因为可能在这个Epoch上,准确率降低了,但是随后的Epoch准确率又升高了,所以不能根据一两次的连续降低就判断不再提高。
|
||||
|
||||
对模型进行训练的过程即是对模型的参数进行学习更新的过程,这个参数学习的过程往往会用到一些迭代方法,如梯度下降(Gradient descent)学习算法。Early stopping便是一种迭代次数截断的方法来防止过拟合的方法,即在模型对训练数据集迭代收敛之前停止迭代来防止过拟合。
|
||||
|
||||
### 16.4.2 理论基础
|
||||
|
||||
早停法,实际上也是一种正则化的策略,可以理解为在网络训练不断逼近最优解的过程种(实际上这个最优解是过拟合的),在梯度等高线的外围就停止了训练,所以其原理上和L2正则是一样的,区别在于得到解的过程。
|
||||
|
||||
我们把图16-21再拿出来讨论一下。
|
||||
|
||||
<img src="./img/16/regular0.png" />
|
||||
|
||||
图16-21 损失函数值的等高线图
|
||||
|
||||
图中所示的等高线图,是当前带噪音的样本点所组成梯度图,并不代表测试集数据,所以其中心位置也不代表这个问题的最优解。我们假设红线是最优解,则早停法的目的就是在到达红线附近时停止训练。
|
||||
|
||||
### 16.4.3 算法
|
||||
|
||||
一般的做法是,在训练的过程中,记录到目前为止最好的validation 准确率,当连续N次Epoch(比如N=10或者更多次)没达到最佳准确率时,则可以认为准确率不再提高了。此时便可以停止迭代了(Early Stopping)。这种策略也称为“No-improvement-in-N”,N即Epoch的次数,可以根据实际情况取,如10、20、30……
|
||||
|
||||
算法描述如下:
|
||||
|
||||
***
|
||||
|
||||
```
|
||||
初始化
|
||||
初始权重均值参数:theta = theta_0
|
||||
迭代次数:i = 0
|
||||
忍耐次数:patience = N (e.g. N=10)
|
||||
忍耐次数计数器:counter = 0
|
||||
验证集损失函数值:lastLoss = 10000 (给一个特别大的数值)
|
||||
|
||||
while (epoch < maxEpoch) 循环迭代训练过程
|
||||
正向计算,反向传播更新theta
|
||||
迭代次数加1:i++
|
||||
计算验证集损失函数值:newLoss = loss
|
||||
if (newLoss < lastLoss) // 新的损失值更小
|
||||
忍耐次数计数器归零:counter = 0
|
||||
记录当前最佳权重矩阵训练参数:theta_best = theta
|
||||
记录当前迭代次数:i_best = i
|
||||
更新最新验证集损失函数值:lastLoss = newLoss
|
||||
else // 新的损失值大于上一步的损失值
|
||||
忍耐次数计数器加1:counter++
|
||||
if (counter >= patience) 停止训练!!!
|
||||
end if
|
||||
end while
|
||||
```
|
||||
|
||||
***
|
||||
|
||||
此时,`theta_best`和`i_best`就是最佳权重值和迭代次数。
|
||||
|
||||
#### 要注意的问题
|
||||
|
||||
1. 门限值`patience`不能太小,比如小于5,因为很可能在5个`epoch`之外,损失函数值又会再次下降
|
||||
2. `patience`不能太大,比如大于30,因为在这30个`epoch`之内,由于样本数量少和数据`shuffle`的关系,很可能某个`epoch`的损失函数值会比上一次低,这样忍耐次数计数器`counter`就清零了,从而不能及时停止。
|
||||
3. 当样本数量少时,为了获得平滑的变化曲线,可以考虑使用加权平均的方式处理当前和历史损失函数值,以避免某一次的高低带来的影响。
|
||||
|
||||
### 16.4.4 实现
|
||||
|
||||
首先,在`TrainingTrace`类中,增加以下成员以支持早停机制:
|
||||
|
||||
- `early_stop`:True表示激活早停机制判断
|
||||
- `patience`:忍耐次数上限,缺省值为5次
|
||||
- `patience_counter`:忍耐次数计数器
|
||||
- `last_vld_loss`:到目前为止最小的验证集损失值
|
||||
|
||||
```Python
|
||||
class TrainingTrace(object):
|
||||
def __init__(self, need_earlyStop = False, patience = 5):
|
||||
......
|
||||
# for early stop
|
||||
self.early_stop = need_earlyStop
|
||||
self.patience = patience
|
||||
self.patience_counter = 0
|
||||
self.last_vld_loss = float("inf")
|
||||
|
||||
def Add(self, epoch, total_iteration, loss_train, accuracy_train, loss_vld, accuracy_vld):
|
||||
......
|
||||
if self.early_stop:
|
||||
if loss_vld < self.last_vld_loss:
|
||||
self.patience_counter = 0
|
||||
self.last_vld_loss = loss_vld
|
||||
else:
|
||||
self.patience_counter += 1
|
||||
if self.patience_counter >= self.patience:
|
||||
return True # need to stop
|
||||
# end if
|
||||
return False
|
||||
```
|
||||
接下来在Add()函数的代码中,如果激活了early_stop机制,则:
|
||||
|
||||
1. 判断loss_vld是否小于last_vld_loss,如果是,清零计数器,保存最新loss值
|
||||
2. 如果否,计数器加1,判断是否达到门限值,是的话返回True,否则返回False
|
||||
|
||||
在main过程中,设置超参时指定正则项为RegularMethod.EarlyStop,并且value=8 (即门限值为8)。
|
||||
|
||||
```Python
|
||||
from Level0_OverFitNet import *
|
||||
|
||||
if __name__ == '__main__':
|
||||
dr = LoadData()
|
||||
hp, num_hidden = SetParameters()
|
||||
hp.regular_name = RegularMethod.EarlyStop
|
||||
hp.regular_value = 8
|
||||
net = Model(dr, 1, num_hidden, 1, hp)
|
||||
ShowResult(net, dr, hp.toString())
|
||||
```
|
||||
|
||||
注意,我们仍然使用和过拟合试验中一样的神经网络,宽度深度不变,只是增加了早停逻辑。
|
||||
|
||||
运行程序后,训练只迭代了2500多次就停止了,和我们预想的一样,损失值和准确率的曲线如图16-22所示。
|
||||
|
||||
<img src="./img/16/EarlyStop_sin_loss.png" />
|
||||
|
||||
图16-22 训练过程中损失函数值和准确率的变化曲线
|
||||
|
||||
早停法并不会提高准确率,而只是在最高的准确率上停止训练(前提是知道后面的训练会造成过拟合),从上图可以看到,最高的准确率是99.07%,达到了我们的目的。
|
||||
|
||||
最后的拟合效果如图16-23所示。
|
||||
|
||||
<img src="./img/16/EarlyStop_sin_result.png" ch="500" />
|
||||
|
||||
图16-23 拟合后的曲线与训练数据的分布图
|
||||
|
||||
蓝点是样本,绿点是理想的拟合效果,红线是实际的拟合效果。
|
||||
|
||||
### 16.4.5 后续的步骤
|
||||
|
||||
在得到早停的迭代次数和权重矩阵参数后,后续有几种方法可以选择。
|
||||
|
||||
#### 彻底停止
|
||||
|
||||
就是啥也不做了,最多再重复几次早停的试验,看看是不是稳定,然后就使用$\theta_{best}$做为训练结果。
|
||||
|
||||
#### 再次训练
|
||||
|
||||
由于第一次早停是通过验证集计算loss值来实现的,所以这次不再分训练集和验证集,记住了早停时的迭代次数,可以重新初始化权重矩阵参数,使用所有数据再次训练,然后到达第一次的$i_{best}$时停止。
|
||||
|
||||
但是由于样本多了,更新批次也会变多,所以可以比较两种策略:
|
||||
|
||||
1) 总迭代次数`epoch`保持不变
|
||||
2) 总更新梯度的次数保持不变
|
||||
|
||||
优点:使用更多的样本可以达到更好的泛化能力。
|
||||
|
||||
缺点:需要重新花时间训练。
|
||||
|
||||
#### 继续训练
|
||||
|
||||
得到$\theta_{best}$后,用全部训练数据(不再分训练集和验证集),在此基础上继续训练若干轮,并且继续用以前的验证集来监控损失函数值,如果能得到比以前更低的损失值,将会是比较理想的情况。
|
||||
|
||||
优点:可以避免重新训练的成本。
|
||||
|
||||
缺点:有可能不能达到目的,损失值降不到理想位置,从而不能终止训练。
|
||||
|
||||
### 代码位置
|
||||
|
||||
|
|
|
@ -3,175 +3,15 @@
|
|||
|
||||
## 16.5 丢弃法 Dropout
|
||||
|
||||
以下为本小节目录,详情请参阅《智能之门》正版图书,高等教育出版社。
|
||||
|
||||
### 16.5.1 基本原理
|
||||
|
||||
2012年,Alex、Hinton在其论文《ImageNet Classification with Deep Convolutional Neural Networks》中用到了Dropout算法,用于防止过拟合。
|
||||
|
||||
我们假设原来的神经网络是这个结构,最后输出三分类结果,如图16-24所示。
|
||||
|
||||
<img src="./img/16/dropout_before.png" />
|
||||
|
||||
图16-24 输出三分类的神经网络结构图
|
||||
|
||||
Dropout可以作为训练深度神经网络的一种正则方法供选择。在每个训练批次中,通过忽略一部分的神经元(让其隐层节点值为0),可以明显地减少过拟合现象。这种方式可以减少隐层节点间的相互作用,高层的神经元需要低层的神经元的输出才能发挥作用,如果高层神经元过分依赖某个低层神经元,就会有过拟合发生。在一次正向/反向的过程中,通过随机丢弃一些神经元,迫使高层神经元和其它的一些低层神经元协同工作,可以有效地防止神经元因为接收到过多的同类型参数而陷入过拟合的状态,来提高泛化程度。
|
||||
|
||||
丢弃后的结果如图16-25所示。
|
||||
|
||||
<img src="./img/16/dropout_after.png" />
|
||||
|
||||
图16-25 使用丢弃法的神经网络结构图
|
||||
|
||||
其中有叉子的神经元在本次迭代训练中被暂时的封闭了,在下一次迭代训练中,再随机地封闭一些神经元,同一个神经元也许被连续封闭两次,也许一次都没有被封闭,完全随机。封闭多少个神经元是由一个超参来控制的,叫做丢弃率。
|
||||
|
||||
### 16.5.2 算法与实现
|
||||
|
||||
#### 前向计算
|
||||
|
||||
正常的隐层计算公式是:
|
||||
|
||||
$$
|
||||
Z = W \cdot X + B \tag{1}
|
||||
$$
|
||||
|
||||
加入随机丢弃步骤后,变成了:
|
||||
|
||||
$$
|
||||
r \sim Bernoulli(p) \tag{2}
|
||||
$$
|
||||
$$Y = r \cdot X \tag{3}$$
|
||||
$$
|
||||
Z = Y \cdot W + B \tag{4}
|
||||
$$
|
||||
|
||||
公式2是得到一个分布概率为p的伯努利分布,伯努利分布在这里可以简单地理解为0-1分布,$p=0.5$时,会以相同概率产生0、1,假设一共10个数,则:
|
||||
$$
|
||||
r=[0,0,1,1,0,1,0,1,1,0]
|
||||
$$
|
||||
或者
|
||||
$$
|
||||
r=[0,1,1,0,0,1,0,1,0,1]
|
||||
$$
|
||||
或者其它一些分布。
|
||||
|
||||
从公式3,Y将会是X经过r的mask的结果,1的位置保留原x值,0的位置相乘后为0。
|
||||
|
||||
#### 反向传播
|
||||
|
||||
在反向传播时,和Relu函数的反向差不多,需要记住正向计算时得到的mask值,反向的误差矩阵直接乘以这个mask值就可以了。
|
||||
|
||||
#### 训练和测试/阶段的不同
|
||||
|
||||
在训练阶段,我们使用正向计算的逻辑。在测试时,不能随机丢弃一些神经元,否则会造成测试结果不稳定,比如某个样本的第一次测试,得到了结果A;第二次测试,得到结果B。由于丢弃的神经元的不同,A和B肯定不相同,就会造成无法解释的情况。
|
||||
|
||||
但是如何模拟那些在训练时丢弃的神经元呢?我们仍然可以利用训练时的丢弃概率,如图16-26所示。
|
||||
|
||||
<img src="./img/16/dropout_neuron.png" />
|
||||
|
||||
图16-26 利用训练时的丢弃概率模拟丢弃的神经元
|
||||
|
||||
图16-26的左侧部分为训练时,输入的信号会以概率p存在,如果$p=0.6$,则会有40%的概率被丢弃,此神经元被封闭;有60%的概率存在,此神经元可以接收到输入并向后传播。
|
||||
|
||||
图16-26的右侧部分为测试/推理时,输入信号总会存在,但是在每个输出上,都应该用原始的权重值,乘以概率p。比如`input=1`,权重值`w=0.12`,`p=0.4`,则output$=1 \times 0.4 \times 0.12=0.048$。
|
||||
|
||||
#### 代码实现
|
||||
|
||||
```Python
|
||||
class DropoutLayer(CLayer):
|
||||
def __init__(self, input_size, ratio=0.5):
|
||||
self.dropout_ratio = ratio
|
||||
self.mask = None
|
||||
self.input_size = input_size
|
||||
self.output_size = input_size
|
||||
|
||||
def forward(self, input, train=True):
|
||||
assert(input.ndim == 2)
|
||||
if train:
|
||||
self.mask = np.random.rand(*input.shape) > self.dropout_ratio
|
||||
self.z = input * self.mask
|
||||
else:
|
||||
self.z = input * (1.0 - self.dropout_ratio)
|
||||
|
||||
return self.z
|
||||
|
||||
def backward(self, delta_in, idx):
|
||||
delta_out = self.mask * delta_in
|
||||
return delta_out
|
||||
```
|
||||
|
||||
上面的代码中,`ratio`是丢弃率,如果`ratio=0.4`,则前面的原理解释中的`p=0.6`。
|
||||
|
||||
另外,我们可以看到,这里的`DropoutLayer`是作为一个层出现的,而不是寄生在全连接层内部。
|
||||
|
||||
写好`Dropout`层后,我们在原来的模型的基础上,搭建一个带`Dropout`层的新模型,如图16-27所示。
|
||||
|
||||
<img src="./img/16/dropout_net.png" />
|
||||
|
||||
图16-27 带`Dropout`层的模型结构图
|
||||
|
||||
与前面的过拟合的网络相比,只是在每个层之间增加一个`Drouput`层。用代码理解的话,请看下面的函数:
|
||||
|
||||
```Python
|
||||
def Model_Dropout(dataReader, num_input, num_hidden, num_output, params):
|
||||
net = NeuralNet41(params, "overfitting")
|
||||
|
||||
fc1 = FcLayer(num_input, num_hidden, params)
|
||||
net.add_layer(fc1, "fc1")
|
||||
s1 = ActivatorLayer(Sigmoid())
|
||||
net.add_layer(s1, "s1")
|
||||
|
||||
d1 = DropoutLayer(num_hidden, 0.1)
|
||||
net.add_layer(d1, "d1")
|
||||
|
||||
fc2 = FcLayer(num_hidden, num_hidden, params)
|
||||
net.add_layer(fc2, "fc2")
|
||||
t2 = ActivatorLayer(Tanh())
|
||||
net.add_layer(t2, "t2")
|
||||
|
||||
#d2 = DropoutLayer(num_hidden, 0.2)
|
||||
#net.add_layer(d2, "d2")
|
||||
|
||||
fc3 = FcLayer(num_hidden, num_hidden, params)
|
||||
net.add_layer(fc3, "fc3")
|
||||
t3 = ActivatorLayer(Tanh())
|
||||
net.add_layer(t3, "t3")
|
||||
|
||||
d3 = DropoutLayer(num_hidden, 0.2)
|
||||
net.add_layer(d3, "d3")
|
||||
|
||||
fc4 = FcLayer(num_hidden, num_output, params)
|
||||
net.add_layer(fc4, "fc4")
|
||||
|
||||
net.train(dataReader, checkpoint=100, need_test=True)
|
||||
net.ShowLossHistory(XCoordinate.Epoch)
|
||||
|
||||
return net
|
||||
```
|
||||
|
||||
运行程序,最后可以得到这样的损失函数图和验证结果,如图16-28所示。
|
||||
|
||||
<img src="./img/16/dropout_sin_loss.png" />
|
||||
|
||||
图16-28 训练过程中损失函数值和准确率的变化曲线
|
||||
|
||||
可以提高精确率到98.17%。
|
||||
|
||||
拟合效果如图16-29所示。
|
||||
|
||||
<img src="./img/16/dropout_sin_result.png" ch="500" />
|
||||
|
||||
图16-29 拟合后的曲线与训练数据的分布图
|
||||
|
||||
### 16.5.3 更好地理解Dropout
|
||||
|
||||
#### 对Dropout的直观理解
|
||||
|
||||
关于Dropout,论文中没有给出任何数学解释,Hintion的直观解释和理由如下:
|
||||
|
||||
1. 由于每次用输入网络的样本进行权值更新时,隐含节点都是以一定概率随机出现,因此不能保证每2个隐含节点每次都同时出现,这样权值的更新不再依赖于有固定关系隐含节点的共同作用,阻止了某些特征仅仅在其它特定特征下才有效果的情况。
|
||||
|
||||
2. 可以将Dropout看作是模型平均的一种。对于每次输入到网络中的样本(可能是一个样本,也可能是一个batch的样本),其对应的网络结构都是不同的,但所有的这些不同的网络结构又同时share隐含节点的权值。这样不同的样本就对应不同的模型,是Bagging方法的一种极端情况。
|
||||
|
||||
3. 还有一个比较有意思的解释是,Dropout类似于性别在生物进化中的角色,物种为了使适应不断变化的环境,性别的出现有效地阻止了过拟合,即避免环境改变时物种可能面临的灭亡。由于性别是一半一半的比例,所以Dropout中的p一般设置为0.5。
|
||||
|
||||
### 代码位置
|
||||
|
||||
|
|
|
@ -3,204 +3,26 @@
|
|||
|
||||
## 17.1 卷积的前向计算
|
||||
|
||||
以下为本小节目录,详情请参阅《智能之门》正版图书,高等教育出版社。
|
||||
|
||||
### 17.1.1 卷积的数学定义
|
||||
|
||||
#### 连续定义
|
||||
|
||||
$$h(x)=(f*g)(x) = \int_{-\infty}^{\infty} f(t)g(x-t)dt \tag{1}$$
|
||||
|
||||
卷积与傅里叶变换有着密切的关系。利用这点性质,即两函数的傅里叶变换的乘积等于它们卷积后的傅里叶变换,能使傅里叶分析中许多问题的处理得到简化。
|
||||
|
||||
#### 离散定义
|
||||
|
||||
$$h(x) = (f*g)(x) = \sum^{\infty}_{t=-\infty} f(t)g(x-t) \tag{2}$$
|
||||
|
||||
### 17.1.2 一维卷积实例
|
||||
|
||||
有两枚骰子$f,g$,掷出后二者相加为4的概率如何计算?
|
||||
|
||||
第一种情况:$f(1)g(3), 3+1=4$,如图17-9所示。
|
||||
|
||||
<img src="./img/17/touzi1.png" />
|
||||
|
||||
图17-9 第一种情况
|
||||
|
||||
第二种情况:$f(2)g(2), 2+2=4$,如图17-10所示。
|
||||
|
||||
<img src="./img/17/touzi2.png" />
|
||||
|
||||
图17-10 第二种情况
|
||||
|
||||
第三种情况:$f(3)g(1), 1+3=4$,如图17-11所示。
|
||||
|
||||
<img src="./img/17/touzi3.png" />
|
||||
|
||||
图17-11 第三种情况
|
||||
|
||||
因此,两枚骰子点数加起来为4的概率为:
|
||||
|
||||
$$
|
||||
\begin{aligned}
|
||||
h(4) &= f(1)g(3)+f(2)g(2)+f(3)g(1) \\\\
|
||||
&=f(1)g(4-1) + f(2)g(4-2) + f(3)g(4-3)
|
||||
\end{aligned}
|
||||
$$
|
||||
|
||||
符合卷积的定义,把它写成标准的形式就是公式2:
|
||||
|
||||
$$h(4)=(f*g)(4)=\sum _{t=1}^{3}f(t)g(4-t)$$
|
||||
|
||||
### 17.1.3 单入单出的二维卷积
|
||||
|
||||
二维卷积一般用于图像处理上。在二维图片上做卷积,如果把图像Image简写为$I$,把卷积核Kernal简写为$K$,则目标图片的第$(i,j)$个像素的卷积值为:
|
||||
|
||||
$$
|
||||
h(i,j) = (I*K)(i,j)=\sum_m \sum_n I(m,n)K(i-m,j-n) \tag{3}
|
||||
$$
|
||||
|
||||
可以看出,这和一维情况下的公式2是一致的。从卷积的可交换性,我们可以把公式3等价地写作:
|
||||
|
||||
$$
|
||||
h(i,j) = (I*K)(i,j)=\sum_m \sum_n I(i-m,j-n)K(m,n) \tag{4}
|
||||
$$
|
||||
|
||||
公式4的成立,是因为我们将Kernal进行了翻转。在神经网络中,一般会实现一个互相关函数(corresponding function),而卷积运算几乎一样,但不反转Kernal:
|
||||
|
||||
$$
|
||||
h(i,j) = (I*K)(i,j)=\sum_m \sum_n I(i+m,j+n)K(m,n) \tag{5}
|
||||
$$
|
||||
|
||||
在图像处理中,自相关函数和互相关函数定义如下:
|
||||
|
||||
- 自相关:设原函数是f(t),则$h=f(t) \star f(-t)$,其中$\star$表示卷积
|
||||
- 互相关:设两个函数分别是f(t)和g(t),则$h=f(t) \star g(-t)$
|
||||
|
||||
互相关函数的运算,是两个序列滑动相乘,两个序列都不翻转。卷积运算也是滑动相乘,但是其中一个序列需要先翻转,再相乘。所以,从数学意义上说,机器学习实现的是互相关函数,而不是原始含义上的卷积。但我们为了简化,把公式5也称作为卷积。这就是卷积的来源。
|
||||
|
||||
结论:
|
||||
|
||||
1. 我们实现的卷积操作不是原始数学含义的卷积,而是工程上的卷积,可以简称为卷积
|
||||
2. 在实现卷积操作时,并不会反转卷积核
|
||||
|
||||
在传统的图像处理中,卷积操作多用来进行滤波,锐化或者边缘检测啥的。我们可以认为卷积是利用某些设计好的参数组合(卷积核)去提取图像空域上相邻的信息。
|
||||
|
||||
按照公式5,我们可以在4x4的图片上,用一个3x3的卷积核,通过卷积运算得到一个2x2的图片,运算的过程如图17-12所示。
|
||||
|
||||
<img src="./img/17/conv_w3_s1.png" ch="526" />
|
||||
|
||||
图17-12 卷积运算的过程
|
||||
###
|
||||
|
||||
### 17.1.4 单入多出的升维卷积
|
||||
|
||||
原始输入是一维的图片,但是我们可以用多个卷积核分别对其计算,从而得到多个特征输出。如图17-13所示。
|
||||
|
||||
<img src="./img/17/conv_2w3.png" ch="500" />
|
||||
|
||||
图17-13 单入多出的升维卷积
|
||||
|
||||
一张4x4的图片,用两个卷积核并行地处理,输出为2个2x2的图片。在训练过程中,这两个卷积核会完成不同的特征学习。
|
||||
|
||||
### 17.1.5 多入单出的降维卷积
|
||||
|
||||
一张图片,通常是彩色的,具有红绿蓝三个通道。我们可以有两个选择来处理:
|
||||
|
||||
1. 变成灰度的,每个像素只剩下一个值,就可以用二维卷积
|
||||
2. 对于三个通道,每个通道都使用一个卷积核,分别处理红绿蓝三种颜色的信息
|
||||
|
||||
显然第2种方法可以从图中学习到更多的特征,于是出现了三维卷积,即有三个卷积核分别对应书的三个通道,三个子核的尺寸是一样的,比如都是2x2,这样的话,这三个卷积核就是一个3x2x2的立体核,称为过滤器Filter,所以称为三维卷积。
|
||||
|
||||
<img src="./img/17/multiple_filter.png" />
|
||||
|
||||
图17-14 多入单出的降维卷积
|
||||
|
||||
在上图中,每一个卷积核对应着左侧相同颜色的输入通道,三个过滤器的值并不一定相同。对三个通道各自做卷积后,得到右侧的三张特征图,然后再按照原始值不加权地相加在一起,得到最右侧的白色特征图,这张图里面已经把三种颜色的特征混在一起了,所以画成了白色,表示没有颜色特征了。
|
||||
|
||||
虽然输入图片是多个通道的,或者说是三维的,但是在相同数量的过滤器的计算后,相加在一起的结果是一个通道,即2维数据,所以称为降维。这当然简化了对多通道数据的计算难度,但同时也会损失多通道数据自带的颜色信息。
|
||||
|
||||
### 17.1.6 多入多出的同维卷积
|
||||
|
||||
在上面的例子中,是一个过滤器Filter内含三个卷积核Kernal。我们假设有一个彩色图片为3x3的,如果有两组3x2x2的卷积核的话,会做什么样的卷积计算?看图17-15。
|
||||
|
||||
<img src="./img/17/conv3dp.png" ch="500" />
|
||||
|
||||
图17-15 多入多出的卷积运算
|
||||
|
||||
第一个过滤器Filter-1为棕色所示,它有三卷积核(Kernal),命名为Kernal-1,Keanrl-2,Kernal-3,分别在红绿蓝三个输入通道上进行卷积操作,生成三个2x2的输出Feature-1,n。然后三个Feature-1,n相加,并再加上b1偏移值,形成最后的棕色输出Result-1。
|
||||
|
||||
对于灰色的过滤器Filter-2也是一样,先生成三个Feature-2,n,然后相加再加b2,最后得到Result-2。
|
||||
|
||||
之所以Feature-m,n还用红绿蓝三色表示,是因为在此时,它们还保留着红绿蓝三种色彩的各自的信息,一旦相加后得到Result,这种信息就丢失了。
|
||||
|
||||
### 17.1.7 卷积编程模型
|
||||
|
||||
上图侧重于解释数值计算过程,而图17-16侧重于解释五个概念的关系:
|
||||
|
||||
- 输入 Input Channel
|
||||
- 卷积核组 WeightsBias
|
||||
- 过滤器 Filter
|
||||
- 卷积核 kernal
|
||||
- 输出 Feature Map
|
||||
|
||||
<img src="./img/17/conv3d.png" ch="500" />
|
||||
|
||||
图17-16 三通道经过两组过滤器的卷积过程
|
||||
|
||||
在此例中,输入是三维数据(3x32x32),经过2x3x5x5的卷积后,输出为三维(2x28x28),维数并没有变化,只是每一维内部的尺寸有了变化,一般都是要向更小的尺寸变化,以便于简化计算。
|
||||
|
||||
对于三维卷积,有以下特点:
|
||||
|
||||
1. 预先定义输出的feature map的数量,而不是根据前向计算自动计算出来,此例中为2,这样就会有两组WeightsBias
|
||||
2. 对于每个输出,都有一个对应的过滤器Filter,此例中Feature Map-1对应Filter-1
|
||||
3. 每个Filter内都有一个或多个卷积核Kernal,对应每个输入通道(Input Channel),此例为3,对应输入的红绿蓝三个通道
|
||||
4. 每个Filter只有一个Bias值,Filter-1对应b1,Filter-2对应b2
|
||||
5. 卷积核Kernal的大小一般是奇数如:1x1, 3x3, 5x5, 7x7等,此例为5x5
|
||||
|
||||
对于上图,我们可以用在全连接神经网络中的学到的知识来理解:
|
||||
|
||||
1. 每个Input Channel就是特征输入,在上图中是3个
|
||||
2. 卷积层的卷积核相当于隐层的神经元,上图中隐层有2个神经元
|
||||
3. $W(m,n), m=[1,2], n=[1,3]$相当于隐层的权重矩阵$w_{11},w_{12},......$
|
||||
4. 每个卷积核(神经元)有1个偏移值
|
||||
|
||||
### 17.1.8 步长 stride
|
||||
|
||||
前面的例子中,每次计算后,卷积核会向右或者向下移动一个单元,即步长stride = 1。而在图17-17这个卷积操作中,卷积核每次向右或向下移动两个单元,即stride = 2。
|
||||
|
||||
<img src="./img/17/Stride2.png" />
|
||||
|
||||
图17-17 步长为2的卷积
|
||||
|
||||
在后续的步骤中,由于每次移动两格,所以最终得到一个2x2的图片。
|
||||
|
||||
### 17.1.9 填充 padding
|
||||
|
||||
如果原始图为4x4,用3x3的卷积核进行卷积后,目标图片变成了2x2。如果我们想保持目标图片和原始图片为同样大小,该怎么办呢?一般我们会向原始图片周围填充一圈0,然后再做卷积。如图17-18。
|
||||
|
||||
<img src="./img/17/padding.png" ch="500" />
|
||||
|
||||
图17-18 带填充的卷积
|
||||
|
||||
### 17.1.10 输出结果
|
||||
|
||||
综合以上所有情况,可以得到卷积后的输出图片的大小的公式:
|
||||
|
||||
$$
|
||||
H_{Output}= {H_{Input} - H_{Kernal} + 2Padding \over Stride} + 1
|
||||
$$
|
||||
|
||||
$$
|
||||
W_{Output}= {W_{Input} - W_{Kernal} + 2Padding \over Stride} + 1
|
||||
$$
|
||||
|
||||
以图17-17为例:
|
||||
|
||||
$$H_{Output}={5 - 3 + 2 \times 0 \over 2}+1=2$$
|
||||
|
||||
以图17-18为例:
|
||||
|
||||
$$H_{Output}={4 - 3 + 2 \times 1 \over 1}+1=4$$
|
||||
|
||||
两点注意:
|
||||
|
||||
1. 一般情况下,我们用正方形的卷积核,且为奇数
|
||||
2. 如果计算出的输出图片尺寸为小数,则取整,不做四舍五入
|
||||
|
|
|
@ -3,358 +3,17 @@
|
|||
|
||||
## 17.2 卷积前向计算代码实现
|
||||
|
||||
以下为本小节目录,详情请参阅《智能之门》正版图书,高等教育出版社。
|
||||
|
||||
### 17.2.1 卷积核的实现
|
||||
|
||||
卷积核,实际上和全连接层一样,是权重矩阵加偏移向量的组合,区别在于全连接层中的权重矩阵是二维的,偏移矩阵是列向量,而卷积核的权重矩阵是四维的,偏移矩阵是也是列向量。
|
||||
|
||||
```Python
|
||||
class ConvWeightsBias(WeightsBias_2_1):
|
||||
def __init__(self, output_c, input_c, filter_h, filter_w, init_method, optimizer_name, eta):
|
||||
self.FilterCount = output_c
|
||||
self.KernalCount = input_c
|
||||
self.KernalHeight = filter_h
|
||||
self.KernalWidth = filter_w
|
||||
...
|
||||
|
||||
def Initialize(self, folder, name, create_new):
|
||||
self.WBShape = (self.FilterCount, self.KernalCount, self.KernalHeight, self.KernalWidth)
|
||||
...
|
||||
```
|
||||
<img src="./img/17/ConvWeightsBias.png" />
|
||||
|
||||
图17-19 卷积核的组成
|
||||
|
||||
以图17-19为例,各个维度的数值如下:
|
||||
|
||||
- FilterCount=2,第一维,过滤器数量,对应输出通道数。
|
||||
- KernalCount=3,第二维,卷积核数量,对应输入通道数。两个Filter里面的Kernal数必须相同。
|
||||
- KernalHeight=5,KernalWidth=5,卷积核的尺寸,第三维和第四维。同一组WeightsBias里的卷积核尺寸必须相同。
|
||||
|
||||
在初始化函数中,会根据四个参数定义`WBShape`,然后在`CreateNew`函数中,创建相应形状的`Weights`和`Bias`。
|
||||
|
||||
### 17.2.2 卷积前向运算的实现 - 方法1
|
||||
```Python
|
||||
class ConvLayer(CLayer):
|
||||
def forward(self, x, train=True):
|
||||
self.x = x
|
||||
self.batch_size = self.x.shape[0]
|
||||
# 如果有必要的话,先对输入矩阵做padding
|
||||
if self.padding > 0:
|
||||
self.padded = np.pad(...)
|
||||
else:
|
||||
self.padded = self.x
|
||||
#end if
|
||||
self.z = conv_4d(...)
|
||||
return self.z
|
||||
```
|
||||
|
||||
上述代码中的`conv_4d()`函数实现了17.1中定义的四维卷积运算:
|
||||
|
||||
```Python
|
||||
def conv_4d(x, weights, bias, out_h, out_w, stride=1):
|
||||
batch_size = x.shape[0]
|
||||
input_channel = x.shape[1]
|
||||
output_channel = weights.shape[0]
|
||||
filter_height = weights.shape[2]
|
||||
filter_width = weights.shape[3]
|
||||
rs = np.zeros((batch_size, num_output_channel, out_h, out_w))
|
||||
|
||||
for bs in range(batch_size):
|
||||
for oc in range(output_channel):
|
||||
rs[bs,oc] += bias[oc]
|
||||
for ic in range(input_channel):
|
||||
for i in range(out_h):
|
||||
for j in range(out_w):
|
||||
ii = i * stride
|
||||
jj = j * stride
|
||||
for fh in range(filter_height):
|
||||
for fw in range(filter_width):
|
||||
rs[bs,oc,i,j] += x[bs,ic,fh+ii,fw+jj] * weights[oc,ic,fh,fw]
|
||||
```
|
||||
|
||||
上面的函数包含以下几重循环:
|
||||
|
||||
1. 批量数据循环(第一维):`bs in batch_size`,对每个样本进行计算;
|
||||
2. 输出通道循环(第二维):`oc in output_channel`。这里先把`bias`加上了,后加也可以;
|
||||
3. 输入通道循环:`ic in input_channel`;
|
||||
4. 输出图像纵坐标循环:`i in out h`;
|
||||
5. 输出图像横坐标循环:`j in out_w`。循环4和5完成对输出图像的每个点的遍历,在下面的子循环中计算并填充值;
|
||||
6. 卷积核纵向循环(第三维):`fh in filter_height`;
|
||||
7. 卷积核横向循环(第四维):`fw in filter_width`。循环6和7完成卷积核与输入图像的卷积计算,并保存到循环4和5指定的输出图像的点上。
|
||||
|
||||
我们试着运行上面的代码并循环10次,看看它的执行效率如何:
|
||||
|
||||
```
|
||||
Time used for Python: 38.057225465774536
|
||||
```
|
||||
|
||||
出乎我们的预料,在足足等了30多秒后,才返回结果。
|
||||
|
||||
通过试验发现,其运行速度非常慢,如果这样的函数在神经网络训练中被调用几十万次,其性能是非常糟糕的,这也是`Python`做为动态语言的一个缺点。
|
||||
|
||||
### 17.2.3 卷积前向运算的实现 - 方法2
|
||||
|
||||
既然动态语言速度慢,我们把它编译成静态方法,是不是会快一些呢?
|
||||
|
||||
很幸运,有这样一个开源项目:[numba](https://numba.pydata.org/),它可以在运行时把`Python`编译成`C`语言执行,代码是用`C`语言“风格”编写的`Python`代码,而且越像`C`的话,执行速度越快。
|
||||
|
||||
我们先用`pip`安装`numba`包:
|
||||
|
||||
```
|
||||
pip install numba
|
||||
```
|
||||
|
||||
然后在需要运行时编译的函数前面加上一个装饰符:
|
||||
|
||||
```Python
|
||||
@nb.jit(nopython=True)
|
||||
def jit_conv_4d(x, weights, bias, out_h, out_w, stride=1):
|
||||
...
|
||||
```
|
||||
|
||||
为了明确起见,我们把`conv_4d`前面加上一个`jit`前缀,表明这个函数是经过`numba`加速的。然后运行循环10次的测试代码:
|
||||
|
||||
```
|
||||
Time used for Numba: 0.0727994441986084
|
||||
```
|
||||
|
||||
又一次出乎我们的预料,这次只用了0.07秒,比纯`Python`代码快了500多倍!
|
||||
|
||||
但是不要急,我们还需要检查一下其正确性。方法1输出结果为`output1`,Numba编译后的方法输出结果为`output2`,二者都是四维矩阵,我们用`np.allclose()`函数来比较它们的差异:
|
||||
|
||||
```Python
|
||||
print("correctness:", np.allclose(output1, output2, atol=1e-7))
|
||||
```
|
||||
|
||||
得到的结果是:
|
||||
|
||||
```
|
||||
correctness: True
|
||||
```
|
||||
|
||||
`np.allclose`方法逐元素检查两种方法的返回值的差异,如果绝对误差在`1e-7`之内,说明两个返回的四维数组相似度极高,运算结果可信。
|
||||
|
||||
为什么不把所有的Python代码都编译成C代码呢?是因为`numba`的能力有限,并不支持`numpy`的所有函数,所以只能把关键的运算代码设计为独立的函数,然后用`numba`编译执行,函数的输入可以是数组、向量、标量,不能是复杂的自定义结构体或函数指针。
|
||||
|
||||
### 17.2.4 卷积前向运算的实现 - 方法3
|
||||
|
||||
由于卷积操作是原始图片数据与卷积核逐点相乘的结果,所以遍历每个点的运算速度非常慢。在全连接层中,由于是两个矩阵直接相乘,所以速度非常快。我们是否可以把卷积操作转换为矩阵操作呢?
|
||||
|
||||
在Caffe框架中,巧妙地把逐点相乘的运算转换成了矩阵运算,大大提升了程序运行速度。这就是著名的`im2col`函数(我们在代码中命名为`img2col`)。
|
||||
|
||||
```Python
|
||||
def forward_img2col(self, x, train=True):
|
||||
self.x = x
|
||||
self.batch_size = self.x.shape[0]
|
||||
assert(self.x.shape == (self.batch_size, self.InC, self.InH, self.InW))
|
||||
self.col_x = img2col(x, self.FH, self.FW, self.stride, self.padding)
|
||||
self.col_w = self.WB.W.reshape(self.OutC, -1).T
|
||||
self.col_b = self.WB.B.reshape(-1, self.OutC)
|
||||
out1 = np.dot(self.col_x, self.col_w) + self.col_b
|
||||
out2 = out1.reshape(batch_size, self.OutH, self.OutW, -1)
|
||||
self.z = np.transpose(out2, axes=(0, 3, 1, 2))
|
||||
return self.z
|
||||
```
|
||||
|
||||
#### 原理
|
||||
|
||||
我们观察一下图17-20。
|
||||
|
||||
<img src="./img/17/img2col.png" ch="500" />
|
||||
|
||||
图17-20 把卷积运算转换成矩阵运算
|
||||
|
||||
先看上半部分:绿色的$3\times 3$矩阵为输入,经过棕色的卷积核运算后,得到右侧的$2\times 2$的矩阵。
|
||||
|
||||
再看图的下半部分:
|
||||
|
||||
第一步,上半部分中蓝色的虚线圆内的四个元素排列成第1行,形成[0,1,3,4],红色虚线圆内的四个元素排列成第4行[4,5,7,8],中间两行可以从右上角的[1,2,4,5]和左下角的[3,4,6,7]得到。这样,一个$3\times 3$的矩阵,就转换成了一个$4\times 4$的矩阵。也就是把卷积核视野中的每个二维$2\times 2$的数组变成$1\times 4$的向量。
|
||||
|
||||
第二步,把棕色的权重矩阵变成$4\times 1$的向量[3,2,1,0]。
|
||||
|
||||
第三步,把$4\times 4$的矩阵与$4\times 1$的向量相乘,得到$4\times 1$的结果向量[5,11,23,29]。
|
||||
|
||||
第四步:把$4\times 1$的结果变成$2\times 2$的矩阵,就得到了卷积运算的真实结果。
|
||||
|
||||
#### 四维数组的展开
|
||||
|
||||
前面只说明了二维数组的展开形式,四维数组可以用同样的方式展开。
|
||||
|
||||
我们假定有2个输入样本,每个样本含有3个通道,每个通道上是$3\times 3$的数据,则样本的原始形状和展开形状分别是:
|
||||
```
|
||||
x =
|
||||
(样本1) [样本2]
|
||||
(通道1) (通道1)
|
||||
[[[[ 0 1 2] [[[27 28 29]
|
||||
[ 3 4 5] [30 31 32]
|
||||
[ 6 7 8]] [33 34 35]]
|
||||
(通道2) (通道2)
|
||||
[[ 9 10 11] [[36 37 38]
|
||||
[12 13 14] [39 40 41]
|
||||
[15 16 17]] [42 43 44]]
|
||||
(通道3) (通道3)
|
||||
[[18 19 20] [[45 46 47]
|
||||
[21 22 23] [48 49 50]
|
||||
[24 25 26]]] [51 52 53]]]]
|
||||
------------------------------------------
|
||||
col_x =
|
||||
[[0. 1. 3. 4.| 9. 10. 12. 13.| 18. 19. 21. 22.]
|
||||
[ 1. 2. 4. 5.| 10. 11. 13. 14.| 19. 20. 22. 23.]
|
||||
[ 3. 4. 6. 7.| 12. 13. 15. 16.| 21. 22. 24. 25.]
|
||||
[ 4. 5. 7. 8.| 13. 14. 16. 17.| 22. 23. 25. 26.]
|
||||
----------------+----------------+----------------
|
||||
[27. 28. 30. 31.| 36. 37. 39. 40.| 45. 46. 48. 49.]
|
||||
[28. 29. 31. 32.| 37. 38. 40. 41.| 46. 47. 49. 50.]
|
||||
[30. 31. 33. 34.| 39. 40. 42. 43.| 48. 49. 51. 52.]
|
||||
[31. 32. 34. 35.| 40. 41. 43. 44.| 49. 50. 52. 53.]]
|
||||
```
|
||||
|
||||
从生成的$8\times 12$的矩阵中可以观察到:
|
||||
|
||||
- 前4行是样本1的数据,后4行是样本2的数据
|
||||
- 前4列是通道1的数据,中间4列是通道2的数据,后4列是通道3的数据
|
||||
|
||||
#### 权重数组的展开
|
||||
|
||||
对应的四维输入数据,卷积核权重数组也需要是四维的,其原始形状和展开后的形状如下:
|
||||
```
|
||||
weights=
|
||||
(过滤器1) (过滤器2)
|
||||
(卷积核1) (卷积核1)
|
||||
[[[[ 0 1] [[[12 13]
|
||||
[ 2 3]] [14 15]]
|
||||
(卷积核2) (卷积核2)
|
||||
[[ 4 5] [[16 17]
|
||||
[ 6 7]] [18 19]]
|
||||
(卷积核3) (卷积核3)
|
||||
[[ 8 9] [[20 21]
|
||||
[10 11]]] [22 23]]]]
|
||||
---------------------------------------
|
||||
col_w=
|
||||
[[ 0 12]
|
||||
[ 1 13]
|
||||
[ 2 14]
|
||||
[ 3 15]
|
||||
[ 4 16]
|
||||
[ 5 17]
|
||||
[ 6 18]
|
||||
[ 7 19]
|
||||
[ 8 20]
|
||||
[ 9 21]
|
||||
[10 22]
|
||||
[11 23]]
|
||||
```
|
||||
|
||||
至此,展开数组已经可以和权重数组做矩阵相乘了。
|
||||
|
||||
#### 结果数据的处理
|
||||
|
||||
原始数据展开成了$8\times 12$的矩阵,权重展开成了$12\times 2$的矩阵,所以最后的结果是$8\times 2$的矩阵:
|
||||
```
|
||||
[[1035.| 2619.]
|
||||
[1101.| 2829.]
|
||||
[1233.| 3249.]
|
||||
[1299.| 3459.]
|
||||
------+-------
|
||||
[2817.| 8289.]
|
||||
[2883.| 8499.]
|
||||
[3015.| 8919.]
|
||||
[3081.| 9129.]]
|
||||
```
|
||||
这是两个样本的结果。如何把它拆开呢?是简单的左右分开就行了吗?这里要稍微动一下脑筋,推理一下:
|
||||
|
||||
1. 两个样本的原始数组x展开后的矩阵`col_x`是$8\times 12$,计算结果是$8\times 2$,如果原始数据只有一个样本,则展开矩阵`col_x`的形状是$4\times 12$,那么运算结果将会是$4\times 2$。所以,在上面这个$8\times 2$的矩阵中,前4行应该是第一个样本的卷积结果,后4行是第二个样本的卷积结果。
|
||||
2. 如果输出通道只有一个,则权重矩阵`w`展开后的`col_w`只有一列,那么运算结果将会是$8\times 1$;两个输出通道的运算结果是$8\times 2$。所以第一列和第二列应该是两个通道的数据,而不是两个样本的数据。
|
||||
|
||||
也就是说,在这个数组中:
|
||||
|
||||
- 第1列的前4行是第1个样本的第1个通道的输出
|
||||
- 第2列的前4行是第1个样本的第2个通道的输出
|
||||
- 第1列的后4行是第2个样本的第1个通道的输出
|
||||
- 第2列的后4行是第2个样本的第2个通道的输出
|
||||
|
||||
于是我们可以分两步得到正确的矩阵形状:
|
||||
|
||||
1. 先把数据变成2个样本 * 输出高度 * 输出宽度的形状:
|
||||
|
||||
```Python
|
||||
out2 = output.reshape(batch_size, output_height, output_width, -1)
|
||||
```
|
||||
|
||||
得到结果:
|
||||
|
||||
```
|
||||
out2=
|
||||
[[[[1035. 2619.]
|
||||
[1101. 2829.]]
|
||||
[[1233. 3249.]
|
||||
[1299. 3459.]]]
|
||||
[[[2817. 8289.]
|
||||
[2883. 8499.]]
|
||||
[[3015. 8919.]
|
||||
[3081. 9129.]]]]
|
||||
```
|
||||
|
||||
注意现在1035和2619在一个子矩阵中,这是不对的,因为它们应该属于两个通道,所以应该在两个子矩阵中。目前这个结果中的四维数据的顺序是:样本、行、列、通道。于是我们做第二步,把“通道”所在的第4维移到第2维,变成:样本、通道、行、列:
|
||||
|
||||
2. 把第4维数据放到第2维(由于是0-base的,所以是把第3维移到第1维的位置):
|
||||
|
||||
```Python
|
||||
out3 = np.transpose(out2, axes=(0, 3, 1, 2))
|
||||
```
|
||||
|
||||
结果是:
|
||||
|
||||
```
|
||||
conv result=
|
||||
(样本1) (样本2)
|
||||
(通道1) (通道1)
|
||||
[[[[1035. 1101.] [[[2817. 2883.]
|
||||
[1233. 1299.]] [3015. 3081.]]
|
||||
(通道2) (通道2)
|
||||
[[2619. 2829.] [[8289. 8499.]
|
||||
[3249. 3459.]]] [8919. 9129.]]]]
|
||||
```
|
||||
|
||||
#### 验证正确性
|
||||
|
||||
我们可以用17.2.3中的方法1做为基准,如果用本节中的方法3可以得到同样的结果,就说明这种方式是正确的。
|
||||
|
||||
```Python
|
||||
def test_4d_im2col():
|
||||
......
|
||||
f1 = c1.forward_numba(x)
|
||||
f2 = c1.forward_img2col(x)
|
||||
print("correctness:", np.allclose(f1, f2, atol=1e-7))
|
||||
```
|
||||
得到的结果是:
|
||||
```
|
||||
correctness: True
|
||||
```
|
||||
上面的代码,首先生成了一个`ConvLayer`实例,然后分别调用内部实现的`forward_numba()`方法和`forward_img2col()`方法,得到`f1`和`f2`两个卷积结果矩阵,然后比较其数值,最后的返回值为`True`,说明`im2col`方法的正确性。
|
||||
|
||||
#### 性能测试
|
||||
|
||||
下面我们比较一下方法2和方法3的性能。
|
||||
|
||||
```Python
|
||||
def test_performance():
|
||||
...
|
||||
print("compare correctness of method 1 and method 2:")
|
||||
print("forward:", np.allclose(f1, f2, atol=1e-7))
|
||||
```
|
||||
|
||||
上述代码先生成一个`ConvLayer`实例,然后分别调用1000次`forward_numba()`方法和1000次`forward_img2col()`方法,最后得到的结果是:
|
||||
|
||||
```
|
||||
method numba: 11.663846492767334
|
||||
method img2col: 14.926148653030396
|
||||
compare correctness of method 1 and method 2:
|
||||
forward: True
|
||||
```
|
||||
|
||||
`numba`方法会比`im2col`方法快3秒,目前看来`numba`方法稍占优势。但是如果没有`numba`的帮助,`im2col`方法会比方法1快几百倍。
|
||||
|
||||
### 代码位置
|
||||
|
||||
ch17, Level2
|
||||
|
|
|
@ -3,412 +3,27 @@
|
|||
|
||||
## 17.3 卷积层的训练
|
||||
|
||||
同全连接层一样,卷积层的训练也需要从上一层回传的误差矩阵,然后计算:
|
||||
以下为本小节目录,详情请参阅《智能之门》正版图书,高等教育出版社。
|
||||
|
||||
1. 本层的权重矩阵的误差项
|
||||
2. 本层的需要回传到下一层的误差矩阵
|
||||
|
||||
在下面的描述中,我们假设已经得到了从上一层回传的误差矩阵,并且已经经过了激活函数的反向传导。
|
||||
|
||||
### 17.3.1 计算反向传播的梯度矩阵
|
||||
|
||||
正向公式:
|
||||
|
||||
$$Z = W*A+b \tag{0}$$
|
||||
|
||||
其中,W是卷积核,*表示卷积(互相关)计算,A为当前层的输入项,b是偏移(未在图中画出),Z为当前层的输出项,但尚未经过激活函数处理。
|
||||
|
||||
我们举一个具体的例子便于分析。图17-21是正向计算过程。
|
||||
|
||||
<img src="./img/17/conv_forward.png" />
|
||||
|
||||
图17-21 卷积正向运算
|
||||
|
||||
分解到每一项就是下列公式:
|
||||
|
||||
$$z_{11} = w_{11} \cdot a_{11} + w_{12} \cdot a_{12} + w_{21} \cdot a_{21} + w_{22} \cdot a_{22} + b \tag{1}$$
|
||||
|
||||
$$z_{12} = w_{11} \cdot a_{12} + w_{12} \cdot a_{13} + w_{21} \cdot a_{22} + w_{22} \cdot a_{23} + b \tag{2}$$
|
||||
|
||||
$$z_{21} = w_{11} \cdot a_{21} + w_{12} \cdot a_{22} + w_{21} \cdot a_{31} + w_{22} \cdot a_{32} + b \tag{3}$$
|
||||
|
||||
$$z_{22} = w_{11} \cdot a_{22} + w_{12} \cdot a_{23} + w_{21} \cdot a_{32} + w_{22} \cdot a_{33} + b \tag{4}$$
|
||||
|
||||
求损失函数$J$对$a_{11}$的梯度:
|
||||
|
||||
$$
|
||||
\frac{\partial J}{\partial a_{11}}=\frac{\partial J}{\partial z_{11}} \frac{\partial z_{11}}{\partial a_{11}}=\delta_{z11}\cdot w_{11} \tag{5}
|
||||
$$
|
||||
|
||||
上式中,$\delta_{z11}$是从网络后端回传到本层的$z_{11}$单元的梯度。
|
||||
|
||||
求$J$对$a_{12}$的梯度时,先看正向公式,发现$a_{12}$对$z_{11}$和$z_{12}$都有贡献,因此需要二者的偏导数相加:
|
||||
|
||||
$$
|
||||
\frac{\partial J}{\partial a_{12}}=\frac{\partial J}{\partial z_{11}} \frac{\partial z_{11}}{\partial a_{12}}+\frac{\partial J}{\partial z_{12}} \frac{\partial z_{12}}{\partial a_{12}}=\delta_{z11} \cdot w_{12}+\delta_{z12} \cdot w_{11} \tag{6}
|
||||
$$
|
||||
|
||||
最复杂的是求$a_{22}$的梯度,因为从正向公式看,所有的输出都有$a_{22}$的贡献,所以:
|
||||
|
||||
$$
|
||||
\frac{\partial J}{\partial a_{22}}=\frac{\partial J}{\partial z_{11}} \frac{\partial z_{11}}{\partial a_{22}}+\frac{\partial J}{\partial z_{12}} \frac{\partial z_{12}}{\partial a_{22}}+\frac{\partial J}{\partial z_{21}} \frac{\partial z_{21}}{\partial a_{22}}+\frac{\partial J}{\partial z_{22}} \frac{\partial z_{22}}{\partial a_{22}}
|
||||
$$
|
||||
|
||||
$$
|
||||
=\delta_{z11} \cdot w_{22} + \delta_{z12} \cdot w_{21} + \delta_{z21} \cdot w_{12} + \delta_{z22} \cdot w_{11} \tag{7}
|
||||
$$
|
||||
|
||||
同理可得所有$a$的梯度。
|
||||
|
||||
观察公式7中的$w$的顺序,貌似是把原始的卷积核旋转了180度,再与传入误差项做卷积操作,即可得到所有元素的误差项。而公式5和公式6并不完备,是因为二者处于角落,这和卷积正向计算中的padding是相同的现象。因此,我们把传入的误差矩阵Delta-In做一个zero padding,再乘以旋转180度的卷积核,就是要传出的误差矩阵Delta-Out,如图17-22所示。
|
||||
|
||||
<img src="./img/17/conv_backward.png" />
|
||||
|
||||
图17-22 卷积运算中的误差反向传播
|
||||
|
||||
最后可以统一成为一个简洁的公式:
|
||||
|
||||
$$\delta_{out} = \delta_{in} * W^{rot180} \tag{8}$$
|
||||
|
||||
这个误差矩阵可以继续回传到下一层。
|
||||
|
||||
- 当Weights是$3\times 3$时,$\delta_{in}$需要padding=2,即加2圈0,才能和Weights卷积后,得到正确尺寸的$\delta_{out}$
|
||||
- 当Weights是$5\times 5$时,$\delta_{in}$需要padding=4,即加4圈0,才能和Weights卷积后,得到正确尺寸的$\delta_{out}$
|
||||
- 以此类推:当Weights是$N\times N$时,$\delta_{in}$需要padding=N-1,即加N-1圈0
|
||||
|
||||
举例:
|
||||
|
||||
正向时stride=1:$A^{(10 \times 8)}*W^{(5 \times 5)}=Z^{(6 \times 4)}$
|
||||
|
||||
反向时,$\delta_z^{(6 \times 4)} + 4 padding = \delta_z^{(14 \times 12)}$
|
||||
|
||||
然后:$\delta_z^{(14 \times 12)} * W^{rot180(5 \times 5)}= \delta_a^{(10 \times 8)}$
|
||||
|
||||
### 17.3.2 步长不为1时的梯度矩阵还原
|
||||
|
||||
我们先观察一下stride=1和2时,卷积结果的差异如图17-23。
|
||||
|
||||
<img src="./img/17/stride_1_2.png"/>
|
||||
|
||||
图17-23 步长为1和步长为2的卷积结果的比较
|
||||
|
||||
二者的差别就是中间那个结果图的灰色部分。如果反向传播时,传入的误差矩阵是stride=2时的2x2的形状,那么我们只需要把它补上一个十字,变成3x3的误差矩阵,就可以用步长为1的算法了。
|
||||
|
||||
以此类推,如果步长为3时,需要补一个双线的十字。所以,当知道当前的卷积层步长为S(S>1)时:
|
||||
|
||||
1. 得到从上层回传的误差矩阵形状,假设为$M \times N$
|
||||
2. 初始化一个$(M \cdot S) \times (N \cdot S)$的零矩阵
|
||||
3. 把传入的误差矩阵的第一行值放到零矩阵第0行的0,S,2S,3S...位置
|
||||
4. 然后把误差矩阵的第二行的值放到零矩阵第S行的0,S,2S,3S...位置
|
||||
5. ......
|
||||
|
||||
步长为2时,用实例表示就是这样:
|
||||
|
||||
$$
|
||||
\begin{bmatrix}
|
||||
\delta_{11} & 0 & \delta_{12} & 0 & \delta_{13}\\\\
|
||||
0 & 0 & 0 & 0 & 0\\\\
|
||||
\delta_{21} & 0 & \delta_{22} & 0 & \delta_{23}\\\\
|
||||
\end{bmatrix}
|
||||
$$
|
||||
|
||||
步长为3时,用实例表示就是这样:
|
||||
|
||||
$$
|
||||
\begin{bmatrix}
|
||||
\delta_{11} & 0 & 0 & \delta_{12} & 0 & 0 & \delta_{13}\\\\
|
||||
0 & 0 & 0 & 0 & 0 & 0 & 0\\\\
|
||||
0 & 0 & 0 & 0 & 0 & 0 & 0\\\\
|
||||
\delta_{21} & 0 & 0 & \delta_{22} & 0 & 0 & \delta_{23}\\\\
|
||||
\end{bmatrix}
|
||||
$$
|
||||
|
||||
### 17.3.3 有多个卷积核时的梯度计算
|
||||
|
||||
有多个卷积核也就意味着有多个输出通道。
|
||||
|
||||
也就是14.1中的升维卷积,如图17-24。
|
||||
|
||||
<img src="./img/17/conv_2w2.png" ch="500" />
|
||||
|
||||
图17-24 升维卷积
|
||||
|
||||
正向公式:
|
||||
|
||||
$$z_{111} = w_{111} \cdot a_{11} + w_{112} \cdot a_{12} + w_{121} \cdot a_{21} + w_{122} \cdot a_{22}$$
|
||||
|
||||
$$z_{112} = w_{111} \cdot a_{12} + w_{112} \cdot a_{13} + w_{121} \cdot a_{22} + w_{122} \cdot a_{23}$$
|
||||
|
||||
$$z_{121} = w_{111} \cdot a_{21} + w_{112} \cdot a_{22} + w_{121} \cdot a_{31} + w_{122} \cdot a_{32}$$
|
||||
|
||||
$$z_{122} = w_{111} \cdot a_{22} + w_{112} \cdot a_{23} + w_{121} \cdot a_{32} + w_{122} \cdot a_{33}$$
|
||||
|
||||
$$z_{211} = w_{211} \cdot a_{11} + w_{212} \cdot a_{12} + w_{221} \cdot a_{21} + w_{222} \cdot a_{22}$$
|
||||
|
||||
$$z_{212} = w_{211} \cdot a_{12} + w_{212} \cdot a_{13} + w_{221} \cdot a_{22} + w_{222} \cdot a_{23}$$
|
||||
|
||||
$$z_{221} = w_{211} \cdot a_{21} + w_{212} \cdot a_{22} + w_{221} \cdot a_{31} + w_{222} \cdot a_{32}$$
|
||||
|
||||
$$z_{222} = w_{211} \cdot a_{22} + w_{212} \cdot a_{23} + w_{221} \cdot a_{32} + w_{222} \cdot a_{33}$$
|
||||
|
||||
求$J$对$a_{22}$的梯度:
|
||||
|
||||
$$
|
||||
\begin{aligned}
|
||||
\frac{\partial J}{\partial a_{22}}&=\frac{\partial J}{\partial Z_{1}} \frac{\partial Z_{1}}{\partial a_{22}}+\frac{\partial J}{\partial Z_{2}} \frac{\partial Z_{2}}{\partial a_{22}} \\\\
|
||||
&=\frac{\partial J}{\partial z_{111}} \frac{\partial z_{111}}{\partial a_{22}}+\frac{\partial J}{\partial z_{112}} \frac{\partial z_{112}}{\partial a_{22}}+\frac{\partial J}{\partial z_{121}} \frac{\partial z_{121}}{\partial a_{22}}+\frac{\partial J}{\partial z_{122}} \frac{\partial z_{122}}{\partial a_{22}} \\\\
|
||||
&+\frac{\partial J}{\partial z_{211}} \frac{\partial z_{211}}{\partial a_{22}}+\frac{\partial J}{\partial z_{212}} \frac{\partial z_{212}}{\partial a_{22}}+\frac{\partial J}{\partial z_{221}} \frac{\partial z_{221}}{\partial a_{22}}+\frac{\partial J}{\partial z_{222}} \frac{\partial z_{222}}{\partial a_{22}} \\\\
|
||||
&=(\delta_{z111} \cdot w_{122} + \delta_{z112} \cdot w_{121} + \delta_{z121} \cdot w_{112} + \delta_{z122} \cdot w_{111}) \\\\
|
||||
&+(\delta_{z211} \cdot w_{222} + \delta_{z212} \cdot w_{221} + \delta_{z221} \cdot w_{212} + \delta_{z222} \cdot w_{211})\\\\
|
||||
&=\delta_{z1} * W_1^{rot180} + \delta_{z2} * W_2^{rot180}
|
||||
\end{aligned}
|
||||
$$
|
||||
|
||||
因此和公式8相似,先在$\delta_{in}$外面加padding,然后和对应的旋转后的卷积核相乘,再把几个结果相加,就得到了需要前传的梯度矩阵:
|
||||
|
||||
$$\delta_{out} = \sum_m \delta_{in\_m} * W^{rot180}_ m \tag{9}$$
|
||||
|
||||
### 17.3.4 有多个输入时的梯度计算
|
||||
|
||||
当输入层是多个图层时,每个图层必须对应一个卷积核,如图17-25。
|
||||
|
||||
<img src="./img/17/conv_1W222.png" ch="500" />
|
||||
|
||||
图17-25 多个图层的卷积必须有一一对应的卷积核
|
||||
|
||||
所以有前向公式:
|
||||
|
||||
$$
|
||||
\begin{aligned}
|
||||
z_{11} &= w_{111} \cdot a_{111} + w_{112} \cdot a_{112} + w_{121} \cdot a_{121} + w_{122} \cdot a_{122}
|
||||
\\\\
|
||||
&+ w_{211} \cdot a_{211} + w_{212} \cdot a_{212} + w_{221} \cdot a_{221} + w_{222} \cdot a_{222}
|
||||
\end{aligned}
|
||||
\tag{10}
|
||||
$$
|
||||
|
||||
$$
|
||||
\begin{aligned}
|
||||
z_{12} &= w_{111} \cdot a_{112} + w_{112} \cdot a_{113} + w_{121} \cdot a_{122} + w_{122} \cdot a_{123} \\\\
|
||||
&+ w_{211} \cdot a_{212} + w_{212} \cdot a_{213} + w_{221} \cdot a_{222} + w_{222} \cdot a_{223}
|
||||
\end{aligned}\tag{11}
|
||||
$$
|
||||
|
||||
$$
|
||||
\begin{aligned}
|
||||
z_{21} &= w_{111} \cdot a_{121} + w_{112} \cdot a_{122} + w_{121} \cdot a_{131} + w_{122} \cdot a_{132} \\\\
|
||||
&+ w_{211} \cdot a_{221} + w_{212} \cdot a_{222} + w_{221} \cdot a_{231} + w_{222} \cdot a_{232}
|
||||
\end{aligned}\tag{12}
|
||||
$$
|
||||
|
||||
$$
|
||||
\begin{aligned}
|
||||
z_{22} &= w_{111} \cdot a_{122} + w_{112} \cdot a_{123} + w_{121} \cdot a_{132} + w_{122} \cdot a_{133} \\\\
|
||||
&+ w_{211} \cdot a_{222} + w_{212} \cdot a_{223} + w_{221} \cdot a_{232} + w_{222} \cdot a_{233}
|
||||
\end{aligned}\tag{13}
|
||||
$$
|
||||
|
||||
最复杂的情况,求$J$对$a_{122}$的梯度:
|
||||
|
||||
$$
|
||||
\begin{aligned}
|
||||
\frac{\partial J}{\partial a_{111}}&=\frac{\partial J}{\partial z_{11}}\frac{\partial z_{11}}{\partial a_{122}} + \frac{\partial J}{\partial z_{12}}\frac{\partial z_{12}}{\partial a_{122}} + \frac{\partial J}{\partial z_{21}}\frac{\partial z_{21}}{\partial a_{122}} + \frac{\partial J}{\partial z_{22}}\frac{\partial z_{22}}{\partial a_{122}}
|
||||
\\\\
|
||||
&=\delta_{z_{11}} \cdot w_{122} + \delta_{z_{12}} \cdot w_{121} + \delta_{z_{21}} \cdot w_{112} + \delta_{z_{22}} \cdot w_{111}
|
||||
\end{aligned}
|
||||
$$
|
||||
|
||||
泛化以后得到:
|
||||
|
||||
$$\delta_{out1} = \delta_{in} * W_1^{rot180} \tag{14}$$
|
||||
|
||||
求$J$对$a_{222}$的梯度:
|
||||
|
||||
$$
|
||||
\begin{aligned}
|
||||
\frac{\partial J}{\partial a_{211}}&=\frac{\partial J}{\partial z_{11}}\frac{\partial z_{11}}{\partial a_{222}} + \frac{\partial J}{\partial z_{12}}\frac{\partial z_{12}}{\partial a_{222}} + \frac{\partial J}{\partial z_{21}}\frac{\partial z_{21}}{\partial a_{222}} + \frac{\partial J}{\partial z_{22}}\frac{\partial z_{22}}{\partial a_{222}} \\\\
|
||||
&=\delta_{z_{11}} \cdot w_{222} + \delta_{z_{12}} \cdot w_{221} + \delta_{z_{21}} \cdot w_{212} + \delta_{z_{22}} \cdot w_{211}
|
||||
\end{aligned}
|
||||
$$
|
||||
|
||||
泛化以后得到:
|
||||
|
||||
$$\delta_{out2} = \delta_{in} * W_2^{rot180} \tag{15}$$
|
||||
|
||||
### 17.3.5 权重(卷积核)梯度计算
|
||||
|
||||
图17-26展示了我们已经熟悉的卷积正向运算。
|
||||
|
||||
<img src="./img/17/conv_forward.png" />
|
||||
|
||||
图17-26 卷积正向计算
|
||||
|
||||
要求J对w11的梯度,从正向公式可以看到,w11对所有的z都有贡献,所以:
|
||||
|
||||
$$
|
||||
\begin{aligned}
|
||||
\frac{\partial J}{\partial w_{11}} &= \frac{\partial J}{\partial z_{11}}\frac{\partial z_{11}}{\partial w_{11}} + \frac{\partial J}{\partial z_{12}}\frac{\partial z_{12}}{\partial w_{11}} + \frac{\partial J}{\partial z_{21}}\frac{\partial z_{21}}{\partial w_{11}} + \frac{\partial J}{\partial z_{22}}\frac{\partial z_{22}}{\partial w_{11}}
|
||||
\\\\
|
||||
&=\delta_{z11} \cdot a_{11} + \delta_{z12} \cdot a_{12} + \delta_{z21} \cdot a_{21} + \delta_{z22} \cdot a_{22}
|
||||
\end{aligned}
|
||||
\tag{9}
|
||||
$$
|
||||
|
||||
对W22也是一样的:
|
||||
|
||||
$$
|
||||
\begin{aligned}
|
||||
\frac{\partial J}{\partial w_{12}} &= \frac{\partial J}{\partial z_{11}}\frac{\partial z_{11}}{\partial w_{12}} + \frac{\partial J}{\partial z_{12}}\frac{\partial z_{12}}{\partial w_{12}} + \frac{\partial J}{\partial z_{21}}\frac{\partial z_{21}}{\partial w_{12}} + \frac{\partial J}{\partial z_{22}}\frac{\partial z_{22}}{\partial w_{12}}
|
||||
\\\\
|
||||
&=\delta_{z11} \cdot a_{12} + \delta_{z12} \cdot a_{13} + \delta_{z21} \cdot a_{22} + \delta_{z22} \cdot a_{23}
|
||||
\end{aligned}
|
||||
\tag{10}
|
||||
$$
|
||||
|
||||
观察公式8和公式9,其实也是一个标准的卷积(互相关)操作过程,因此,可以把这个过程看成图17-27。
|
||||
|
||||
<img src="./img/17/conv_delta_w.png" />
|
||||
|
||||
图17-27 卷积核的梯度计算
|
||||
|
||||
总结成一个公式:
|
||||
|
||||
$$\delta_w = A * \delta_{in} \tag{11}$$
|
||||
|
||||
### 17.3.6 偏移的梯度计算
|
||||
|
||||
根据前向计算公式1,2,3,4,可以得到:
|
||||
|
||||
$$
|
||||
\begin{aligned}
|
||||
\frac{\partial J}{\partial b} &= \frac{\partial J}{\partial z_{11}}\frac{\partial z_{11}}{\partial b} + \frac{\partial J}{\partial z_{12}}\frac{\partial z_{12}}{\partial b} + \frac{\partial J}{\partial z_{21}}\frac{\partial z_{21}}{\partial b} + \frac{\partial J}{\partial z_{22}}\frac{\partial z_{22}}{\partial b}
|
||||
\\\\
|
||||
&=\delta_{z11} + \delta_{z12} + \delta_{z21} + \delta_{z22}
|
||||
\end{aligned}
|
||||
\tag{12}
|
||||
$$
|
||||
|
||||
所以:
|
||||
|
||||
$$
|
||||
\delta_b = \delta_{in} \tag{13}
|
||||
$$
|
||||
|
||||
每个卷积核W可能会有多个filter,或者叫子核,但是一个卷积核只有一个偏移,无论有多少子核。
|
||||
|
||||
### 17.3.7 计算卷积核梯度的实例说明
|
||||
|
||||
下面我们会用一个简单的例子来说明卷积核的训练过程。我们先制作一张样本图片,然后使用“横边检测”算子做为卷积核对该样本进行卷积,得到对比如图17-28。
|
||||
|
||||
<img src="./img/17/level3_1.png" ch="500" />
|
||||
|
||||
图17-28 原图和经过横边检测算子的卷积结果
|
||||
|
||||
左侧为原始图片(80x80的灰度图),右侧为经过3x3的卷积后的结果图片(78x78的灰度图)。由于算子是横边检测,所以只保留了原始图片中的横边。
|
||||
|
||||
卷积核矩阵:
|
||||
|
||||
$$
|
||||
w=\begin{pmatrix}
|
||||
0 & -1 & 0 \\\\
|
||||
0 & 2 & 0 \\\\
|
||||
0 & -1 & 0
|
||||
\end{pmatrix}
|
||||
$$
|
||||
|
||||
现在我们转换一下问题:假设我们有一张原始图片(如左侧)和一张目标图片(如右侧),我们如何得到对应的卷积核呢?
|
||||
|
||||
我们在前面学习了线性拟合的解决方案,实际上这个问题是同一种性质的,只不过把直线拟合点阵的问题,变成了图像拟合图像的问题,如表17-3所示。
|
||||
|
||||
表17-3 直线拟合与图像拟合的比较
|
||||
|
||||
||样本数据|标签数据|预测数据|公式|损失函数|
|
||||
|---|---|---|---|---|---|
|
||||
|直线拟合|样本点x|标签值y|预测直线z|$z=x \cdot w+b$|均方差|
|
||||
|图片拟合|原始图片x|目标图片y|预测图片z|$z=x * w+b$|均方差|
|
||||
|
||||
直线拟合中的均方差,是计算预测值与样本点之间的距离;图片拟合中的均方差,可以直接计算两张图片对应的像素点之间的差值。
|
||||
|
||||
为了简化问题,我们令b=0,只求卷积核w的值,则前向公式为:
|
||||
|
||||
$$
|
||||
z = x * w
|
||||
$$
|
||||
|
||||
$$
|
||||
loss = \frac{1}{2}(z-y)^2
|
||||
$$
|
||||
|
||||
反向求解w的梯度公式(从公式11得到):
|
||||
|
||||
$$
|
||||
\frac{\partial loss}{\partial w}=\frac{\partial loss}{\partial z}\frac{\partial z}{\partial w}=x * (z-y)
|
||||
$$
|
||||
|
||||
即w的梯度为预测图片z减去目标图片y的结果,再与原始图片x做卷积,其中x为被卷积图片,z-y为卷积核。
|
||||
|
||||
训练部分的代码实现如下:
|
||||
|
||||
```Python
|
||||
def train(x, w, b, y):
|
||||
output = create_zero_array(x, w)
|
||||
for i in range(10000):
|
||||
# forward
|
||||
jit_conv_2d(x, w, b, output)
|
||||
# loss
|
||||
t1 = (output - y)
|
||||
m = t1.shape[0]*t1.shape[1]
|
||||
LOSS = np.multiply(t1, t1)
|
||||
loss = np.sum(LOSS)/2/m
|
||||
print(i,loss)
|
||||
if loss < 1e-7:
|
||||
break
|
||||
# delta
|
||||
delta = output - y
|
||||
# backward
|
||||
dw = np.zeros(w.shape)
|
||||
jit_conv_2d(x, delta, b, dw)
|
||||
w = w - 0.5 * dw/m
|
||||
#end for
|
||||
return w
|
||||
```
|
||||
|
||||
一共迭代10000次:
|
||||
|
||||
1. 用jit_conv_2d(x,w...)做一次前向计算
|
||||
2. 计算loss值以便检测停止条件,当loss值小于1e-7时停止迭代
|
||||
3. 然后计算delta值
|
||||
4. 再用jit_conv_2d(x,delta)做一次反向计算,得到w的梯度
|
||||
5. 最后更新卷积核w的值
|
||||
|
||||
运行结果:
|
||||
|
||||
```
|
||||
......
|
||||
3458 1.0063169744079507e-07
|
||||
3459 1.0031151142628902e-07
|
||||
3460 9.999234418532805e-08
|
||||
w_true:
|
||||
[[ 0 -1 0]
|
||||
[ 0 2 0]
|
||||
[ 0 -1 0]]
|
||||
w_result:
|
||||
[[-1.86879237e-03 -9.97261724e-01 -1.01212359e-03]
|
||||
[ 2.58961697e-03 1.99494606e+00 2.74435794e-03]
|
||||
[-8.67754199e-04 -9.97404263e-01 -1.87580756e-03]]
|
||||
w allclose: True
|
||||
y allclose: True
|
||||
```
|
||||
|
||||
当迭代到3460次的时候,loss值小于1e-7,迭代停止。比较w_true和w_result的值,两者非常接近。用numpy.allclose()方法比较真实卷积核和训练出来的卷积核的值,结果为True。比如-1.86879237e-03,接近于0;-9.97261724e-01,接近于-1。
|
||||
|
||||
再比较卷积结果,当然也会非常接近,误差很小,allclose结果为True。用图示方法显示卷积结果比较如图17-29。
|
||||
|
||||
<img src="./img/17/level3_2.png" ch="500" />
|
||||
|
||||
图17-29 真实值和训练值的卷积结果区别
|
||||
|
||||
人眼是看不出什么差异来的。由此我们可以直观地理解到卷积核的训练过程并不复杂。
|
||||
|
||||
### 代码位置
|
||||
|
||||
|
|
|
@ -3,372 +3,15 @@
|
|||
|
||||
## 17.4 卷积反向传播代码实现
|
||||
|
||||
以下为本小节目录,详情请参阅《智能之门》正版图书,高等教育出版社。
|
||||
|
||||
### 17.4.1 方法1
|
||||
|
||||
完全按照17.3中的讲解来实现反向传播,但是由于有17.2中关于numba帮助,我们在实现代码时,可以考虑把一些模块化的计算放到独立的函数中,用numba在运行时编译加速。
|
||||
|
||||
```Python
|
||||
def backward_numba(self, delta_in, flag):
|
||||
# 如果正向计算中的stride不是1,转换成是1的等价误差数组
|
||||
dz_stride_1 = expand_delta_map(delta_in, ...)
|
||||
# 计算本层的权重矩阵的梯度
|
||||
self._calculate_weightsbias_grad(dz_stride_1)
|
||||
# 由于输出误差矩阵的尺寸必须与本层的输入数据的尺寸一致,所以必须根据卷积核的尺寸,调整本层的输入误差矩阵的尺寸
|
||||
(pad_h, pad_w) = calculate_padding_size(...)
|
||||
dz_padded = np.pad(dz_stride_1, ...)
|
||||
# 计算本层输出到下一层的误差矩阵
|
||||
delta_out = self._calculate_delta_out(dz_padded, flag)
|
||||
#return delta_out
|
||||
return delta_out, self.WB.dW, self.WB.dB
|
||||
|
||||
# 用输入数据乘以回传入的误差矩阵,得到卷积核的梯度矩阵
|
||||
def _calculate_weightsbias_grad(self, dz):
|
||||
self.WB.ClearGrads()
|
||||
# 先把输入矩阵扩大,周边加0
|
||||
(pad_h, pad_w) = calculate_padding_size(...)
|
||||
input_padded = np.pad(self.x, ...)
|
||||
# 输入矩阵与误差矩阵卷积得到权重梯度矩阵
|
||||
(self.WB.dW, self.WB.dB) = calcalate_weights_grad(...)
|
||||
self.WB.MeanGrads(self.batch_size)
|
||||
|
||||
# 用输入误差矩阵乘以(旋转180度后的)卷积核
|
||||
def _calculate_delta_out(self, dz, layer_idx):
|
||||
if layer_idx == 0:
|
||||
return None
|
||||
# 旋转卷积核180度
|
||||
rot_weights = self.WB.Rotate180()
|
||||
# 定义输出矩阵形状
|
||||
delta_out = np.zeros(self.x.shape)
|
||||
# 输入梯度矩阵卷积旋转后的卷积核,得到输出梯度矩阵
|
||||
delta_out = calculate_delta_out(dz, ..., delta_out)
|
||||
|
||||
return delta_out
|
||||
```
|
||||
为了节省篇幅,上面的代码中做了一些省略,只保留了基本的实现思路,并给出了详尽的注释,相信读者在充分理解17.3的内容的基础上,可以看懂。
|
||||
|
||||
其中,两个计算量大的函数,一个是计算权重矩阵的基础函数calcalate_weights_grad,另一个是计算输出误差矩阵的基础函数calculate_delta_out,都使用了numba的方式实现,以加快反向传播代码的运行速度。
|
||||
|
||||
### 17.4.2 方法2
|
||||
|
||||
在前向计算中,我们试验了img2col的方法,取得了不错的效果。在反向传播中,也有对应的逆向方法,叫做col2img。下面我们基于它来实现另外一种反向传播算法,其基本思想是:把反向传播也看作是全连接层的方式,直接用矩阵运算代替卷积操作,然后把结果矩阵再转换成卷积操作的反向传播所需要的形状。
|
||||
|
||||
#### 代码实现
|
||||
|
||||
```Python
|
||||
def backward_col2img(self, delta_in, layer_idx):
|
||||
OutC, InC, FH, FW = self.WB.W.shape
|
||||
# 误差矩阵变换
|
||||
delta_in_2d = np.transpose(delta_in, axes=(0,2,3,1)).reshape(-1, OutC)
|
||||
# 计算Bias的梯度
|
||||
self.WB.dB = np.sum(delta_in_2d, axis=0, keepdims=True).T / self.batch_size
|
||||
# 计算Weights的梯度
|
||||
dW = np.dot(self.col_x.T, delta_in_2d) / self.batch_size
|
||||
# 转换成卷积核的原始形状
|
||||
self.WB.dW = np.transpose(dW, axes=(1, 0)).reshape(OutC, InC, FH, FW)# 计算反向传播误差矩阵
|
||||
dcol = np.dot(delta_in_2d, self.col_w.T)
|
||||
# 转换成与输入数据x相同的形状
|
||||
delta_out = col2img(dcol, self.x.shape, FH, FW, self.stride, self.padding)
|
||||
return delta_out, self.WB.dW, self.WB.dB
|
||||
```
|
||||
|
||||
#### 单样本单通道的实例讲解
|
||||
|
||||
假设有1个样本1个通道且图片为3x3的矩阵:
|
||||
```
|
||||
x=
|
||||
[[[[0 1 2]
|
||||
[3 4 5]
|
||||
[6 7 8]]]]
|
||||
|
||||
col_x=
|
||||
[[0. 1. 3. 4.]
|
||||
[1. 2. 4. 5.]
|
||||
[3. 4. 6. 7.]
|
||||
[4. 5. 7. 8.]]
|
||||
```
|
||||
卷积核也只有1个形状为1x1x2x2的矩阵:
|
||||
```
|
||||
w=
|
||||
[[[[0 1]
|
||||
[2 3]]]]
|
||||
```
|
||||
卷积核展开后:
|
||||
```
|
||||
col_w=
|
||||
[[0]
|
||||
[1]
|
||||
[2]
|
||||
[3]]
|
||||
```
|
||||
卷积的结果会是一个样本在一个通道上的2x2的输出。
|
||||
|
||||
再假设从后端反向传播回来的输入误差矩阵:
|
||||
```
|
||||
delta_in=
|
||||
[[[[0 1]
|
||||
[2 3]]]]
|
||||
```
|
||||
误差矩阵经过下式变换:
|
||||
```Python
|
||||
delta_in_2d = np.transpose(delta_in, axes=(0,2,3,1)).reshape(-1, OutC)
|
||||
```
|
||||
得到:
|
||||
```
|
||||
delta_in_2d=
|
||||
[[0]
|
||||
[1]
|
||||
[2]
|
||||
[3]]
|
||||
```
|
||||
计算dB(这一步和全连接层完全相同):
|
||||
```Python
|
||||
self.WB.dB = np.sum(delta_in_2d, axis=0, keepdims=True).T / self.batch_size
|
||||
```
|
||||
得到:
|
||||
```
|
||||
dB=
|
||||
[[6.]]
|
||||
```
|
||||
计算dW(这一步和全连接层完全相同):
|
||||
```Python
|
||||
dW = np.dot(self.col_x.T, delta_in_2d) / self.batch_size
|
||||
```
|
||||
得到:
|
||||
```
|
||||
dW=
|
||||
[[19.]
|
||||
[25.]
|
||||
[37.]
|
||||
[43.]]
|
||||
```
|
||||
还原dW到1x1x2x2的卷积核形状:
|
||||
```Python
|
||||
self.WB.dW = np.transpose(dW, axes=(1, 0)).reshape(OutC, InC, FH, FW)
|
||||
```
|
||||
得到:
|
||||
```
|
||||
dW=
|
||||
[[[[19. 25.]
|
||||
[37. 43.]]]]
|
||||
```
|
||||
至此,dB和dW都已经得到,本层的梯度计算完毕,需要把梯度回传给前一层,所以要计算输出误差矩阵(这一步和全连接层完全相同):
|
||||
```Python
|
||||
dcol = np.dot(delta_in_2d, self.col_w.T)
|
||||
```
|
||||
得到:
|
||||
```
|
||||
dcol=
|
||||
[[0 0 0 0]
|
||||
[0 1 2 3]
|
||||
[0 2 4 6]
|
||||
[0 3 6 9]]
|
||||
```
|
||||
转换成正确的矩阵形状:
|
||||
```Python
|
||||
delta_out = col2img(dcol, self.x.shape, FH, FW, self.stride, self.padding)
|
||||
```
|
||||
得到:
|
||||
```
|
||||
delta_out=
|
||||
[[[[ 0. 0. 1.]
|
||||
[ 0. 4. 6.]
|
||||
[ 4. 12. 9.]]]]
|
||||
```
|
||||
|
||||
下面我们解释一下最后一步的运算过程,如图17-30所示。
|
||||
|
||||
<img src="./img/17/col2img.png" ch="500" />
|
||||
|
||||
图17-30 col2img图解
|
||||
|
||||
得到上述dcol的4x4矩阵后,我们要把它逆变换到一个3x3的矩阵中,步骤如下:
|
||||
|
||||
1. 左侧第一行红色椭圆内的四个元素移到右侧红色圆形内;
|
||||
2. 在1的基础上,左侧第二行黄色椭圆内的四个元素移到右侧黄色圆形内,其中与原有元素重叠的地方则两个值相加。比如中间那个元素就是0+2=2;
|
||||
3. 在2的基础上,左侧第三行蓝色椭圆内的四个元素移到右侧蓝色圆形内,其中与原有元素重叠的地方则两个值相加。比如中间那个元素再次加2;
|
||||
4. 在3的基础上,左侧第四行绿色椭圆内的四个元素移到右侧绿色圆形内,其中与原有元素重叠的地方则两个值相加,中间的元素再次加0,还是4;中间靠下的元素原值是6,加6后为12。
|
||||
|
||||
这个结果和最后一步delta_out的结果完全一致。
|
||||
|
||||
#### 多样本多通道的实例讲解
|
||||
|
||||
图17-31是两个样本的例子,输入通道为3,输出通道为2。
|
||||
|
||||
<img src="./img/17/conv4d.png" ch="500" />
|
||||
|
||||
图17-31 两个样本三通道两个卷积核的例子
|
||||
|
||||
图17-31中的各组件参数如下:
|
||||
|
||||
- batch size = 2
|
||||
- input channel = 3
|
||||
- input height = 3
|
||||
- input width = 3
|
||||
- filter height = 2
|
||||
- filter width = 2
|
||||
- stride = 1
|
||||
- padding = 0
|
||||
- output channel = 2
|
||||
- output height = 2
|
||||
- output width = 2
|
||||
|
||||
#### 误差输入矩阵
|
||||
|
||||
delta_in是本层的误差输入矩阵,它的形状应该和本层的前向计算结果一样。在本例中,误差输入矩阵的形状应该是:(batch_size * output_channel * output_height * output_width) = (2 x 2 x 2 x 2):
|
||||
```
|
||||
delta_in=
|
||||
(样本1)
|
||||
(通道1)
|
||||
[[[[ 0 1]
|
||||
[ 2 3]]
|
||||
(通道2)
|
||||
[[ 4 5]
|
||||
[ 6 7]]]
|
||||
(样本2)
|
||||
(通道1)
|
||||
[[[ 8 9]
|
||||
[10 11]]
|
||||
(通道2)
|
||||
[[12 13]
|
||||
[14 15]]]]
|
||||
```
|
||||
|
||||
为了做img2col的逆运算col2img,我们把它转换成17.2中的结果数据的形状8x2:
|
||||
|
||||
```Python
|
||||
delta_in_2d = np.transpose(delta_in, axes=(0,2,3,1)).reshape(-1, OutC)
|
||||
```
|
||||
|
||||
```
|
||||
delta_in_2d=
|
||||
[[ 0 4]
|
||||
[ 1 5]
|
||||
[ 2 6]
|
||||
[ 3 7]
|
||||
[ 8 12]
|
||||
[ 9 13]
|
||||
[10 14]
|
||||
[11 15]]
|
||||
```
|
||||
|
||||
计算权重矩阵的梯度:
|
||||
|
||||
```Python
|
||||
dW = np.dot(self.col_x.T, delta_in_2d) / self.batch_size
|
||||
```
|
||||
|
||||
结果:
|
||||
|
||||
```
|
||||
dW=
|
||||
[[ 564.| 812.]
|
||||
[ 586.| 850.]
|
||||
[ 630.| 926.]
|
||||
[ 652.| 964.]
|
||||
------+-------
|
||||
[ 762.| 1154.]
|
||||
[ 784.| 1192.]
|
||||
[ 828.| 1268.]
|
||||
[ 850.| 1306.]
|
||||
------+-------
|
||||
[ 960.| 1496.]
|
||||
[ 982.| 1534.]
|
||||
[1026.| 1610.]
|
||||
[1048.| 1648.]]
|
||||
```
|
||||
但是这个12x2的结果是对应的权重矩阵的二维数组展开形式的,所以要还原成原始的卷积核形式2x3x2x2:
|
||||
|
||||
```Python
|
||||
self.WB.dW = np.transpose(dW, axes=(1, 0)).reshape(OutC, InC, FH, FW)
|
||||
```
|
||||
|
||||
结果:
|
||||
|
||||
```
|
||||
dW=
|
||||
(过滤器1) (过滤器2)
|
||||
(卷积核1) (卷积核1)
|
||||
[[[[ 564. 586.] [[[ 812. 850.]
|
||||
[ 630. 652.]] [ 926. 964.]]
|
||||
(卷积核2) (卷积核2)
|
||||
[[ 762. 784.] [[1154. 1192.]
|
||||
[ 828. 850.]] [1268. 1306.]]
|
||||
(卷积核3) (卷积核3)
|
||||
[[ 960. 982.] [[1496. 1534.]
|
||||
[1026. 1048.]]] [1610. 1648.]]]]
|
||||
```
|
||||
|
||||
计算误差输出矩阵:
|
||||
|
||||
```Python
|
||||
dcol = np.dot(delta_in_2d, self.col_w.T)
|
||||
```
|
||||
|
||||
得到:
|
||||
|
||||
```
|
||||
dcol=
|
||||
[[ 48 52 56 60 64 68 72 76 80 84 88 92]
|
||||
[ 60 66 72 78 84 90 96 102 108 114 120 126]
|
||||
[ 72 80 88 96 104 112 120 128 136 144 152 160]
|
||||
[ 84 94 104 114 124 134 144 154 164 174 184 194]
|
||||
[144 164 184 204 224 244 264 284 304 324 344 364]
|
||||
[156 178 200 222 244 266 288 310 332 354 376 398]
|
||||
[168 192 216 240 264 288 312 336 360 384 408 432]
|
||||
[180 206 232 258 284 310 336 362 388 414 440 466]]
|
||||
```
|
||||
|
||||
但是dcol对应的是输入数据的二维展开形式4x12,应该把它还原成2x3x3x3的形式:
|
||||
|
||||
```Python
|
||||
delta_out = col2img(dcol, self.x.shape, FH, FW, self.stride, self.padding)
|
||||
```
|
||||
|
||||
得到:
|
||||
|
||||
```
|
||||
delta_out=
|
||||
(样本1) (样本2)
|
||||
(通道1) (通道1)
|
||||
[[[[ 48. 112. 66.] [[[ 144. 320. 178.]
|
||||
[ 128. 296. 172.] [ 352. 776. 428.]
|
||||
[ 88. 200. 114.]] [ 216. 472. 258.]]
|
||||
(通道2) (通道2)
|
||||
[[ 64. 152. 90.] [[ 224. 488. 266.]
|
||||
[ 176. 408. 236.] [ 528. 1144. 620.]
|
||||
[ 120. 272. 154.]] [ 312. 672. 362.]]
|
||||
(通道3) (通道3)
|
||||
[[ 80. 192. 114.] [[ 304. 656. 354.]
|
||||
[ 224. 520. 300.] [ 704. 1512. 812.]
|
||||
[ 152. 344. 194.]]] [ 408. 872. 466.]]]]
|
||||
```
|
||||
|
||||
### 17.4.3 正确性与性能测试
|
||||
|
||||
在正向计算中,numba稍胜一筹,下面我们来测试一下二者的反向计算性能,然后比较梯度输出矩阵的结果来验证正确性。
|
||||
|
||||
```Python
|
||||
def test_performance():
|
||||
...
|
||||
```
|
||||
|
||||
先用numba方法测试1000次的正向+反向,然后再测试1000次img2col的正向+反向,同时我们会比较反向传播的三个输出值:误差矩阵b、权重矩阵梯度dw、偏移矩阵梯度db。
|
||||
|
||||
输出结果:
|
||||
|
||||
```
|
||||
method numba: 11.830008506774902
|
||||
method img2col: 3.543151378631592
|
||||
compare correctness of method 1 and method 2:
|
||||
forward: True
|
||||
backward: True
|
||||
dW: True
|
||||
dB: True
|
||||
```
|
||||
这次img2col方法完胜,100次的正向+反向用了3.5秒,而numba方法花费的时间是前者的三倍多。再看二者的前向计算和反向传播的结果比较,四个矩阵的比较全都是True,说明我们的代码是没有问题的。
|
||||
|
||||
那么我们能不能混用numba方法的前向计算和img2col方法的反向传播呢?不行。因为img2col方法的反向传播需要用到其正向计算方法中的两个缓存数组,一个是输入数据的矩阵变换结果,另一个是权重矩阵变换结果,所以img2col方法必须正向反向配合使用。
|
||||
|
||||
### 代码位置
|
||||
|
||||
ch17, Level4
|
||||
|
|
|
@ -3,200 +3,24 @@
|
|||
|
||||
## 17.5 池化层
|
||||
|
||||
以下为本小节目录,详情请参阅《智能之门》正版图书,高等教育出版社。
|
||||
|
||||
### 17.5.1 常用池化方法
|
||||
|
||||
池化 pooling,又称为下采样,downstream sampling or sub-sampling。
|
||||
|
||||
池化方法分为两种,一种是最大值池化 Max Pooling,一种是平均值池化 Mean/Average Pooling。如图17-32所示。
|
||||
|
||||
<img src="./img/17/pooling.png" />
|
||||
|
||||
图17-32 池化
|
||||
|
||||
- 最大值池化,是取当前池化视野中所有元素的最大值,输出到下一层特征图中。
|
||||
- 平均值池化,是取当前池化视野中所有元素的平均值,输出到下一层特征图中。
|
||||
|
||||
其目的是:
|
||||
|
||||
- 扩大视野:就如同先从近处看一张图片,然后离远一些再看同一张图片,有些细节就会被忽略
|
||||
- 降维:在保留图片局部特征的前提下,使得图片更小,更易于计算
|
||||
- 平移不变性,轻微扰动不会影响输出:比如上图中最大值池化的4,即使向右偏一个像素,其输出值仍为4
|
||||
- 维持同尺寸图片,便于后端处理:假设输入的图片不是一样大小的,就需要用池化来转换成同尺寸图片
|
||||
|
||||
一般我们都使用最大值池化。
|
||||
|
||||
### 17.5.2 池化的其它方式
|
||||
|
||||
在上面的例子中,我们使用了size=2x2,stride=2的模式,这是常用的模式,即步长与池化尺寸相同。
|
||||
|
||||
我们很少使用步长值与池化尺寸不同的配置,所以只是提一下,如图17-33。
|
||||
|
||||
<img src="./img/17/pooling2.png" />
|
||||
|
||||
图17-33 步长为1的池化
|
||||
|
||||
上图是stride=1, size=2x2的情况,可以看到,右侧的结果中,有一大堆的3和4,基本分不开了,所以其池化效果并不好。
|
||||
|
||||
假设输入图片的形状是 $W_1 \times H_1 \times D_1$,其中W是图片宽度,H是图片高度,D是图片深度(多个图层),F是池化的视野(正方形),S是池化的步长,则输出图片的形状是:
|
||||
|
||||
$$
|
||||
\begin{cases}
|
||||
W_2 = (W_1 - F)/S + 1 \\\\
|
||||
H_2 = (H_1 - F)/S + 1 \\\\
|
||||
D_2 = D_1
|
||||
\end{cases}
|
||||
$$
|
||||
|
||||
池化层不会改变图片的深度,即D值前后相同。
|
||||
|
||||
### 17.5.3 池化层的训练
|
||||
|
||||
我们假设图17-34中,$[[1,2],[3,4]]$是上一层网络回传的残差,那么:
|
||||
|
||||
- 对于最大值池化,残差值会回传到当初最大值的位置上,而其它三个位置的残差都是0。
|
||||
- 对于平均值池化,残差值会平均到原始的4个位置上。
|
||||
|
||||
<img src="./img/17/pooling_backward.png" />
|
||||
|
||||
图17-34 平均池化与最大池化
|
||||
|
||||
<img src="./img/17/pooling_backward_max.png" />
|
||||
|
||||
图17-35 池化层反向传播的示例
|
||||
|
||||
#### Max Pooling
|
||||
|
||||
严格的数学推导过程以图17-35为例进行。
|
||||
|
||||
正向公式:
|
||||
|
||||
$$
|
||||
w = max(a,b,e,f)
|
||||
$$
|
||||
|
||||
反向公式(假设Input Layer中的最大值是b):
|
||||
|
||||
$$
|
||||
{\partial w \over \partial a} = 0, \quad {\partial w \over \partial b} = 1
|
||||
$$
|
||||
|
||||
$$
|
||||
{\partial w \over \partial e} = 0, \quad {\partial w \over \partial f} = 0
|
||||
$$
|
||||
|
||||
因为a,e,f对w都没有贡献,所以偏导数为0,只有b有贡献,偏导数为1。
|
||||
|
||||
$$
|
||||
\delta_a = {\partial J \over \partial a} = {\partial J \over \partial w} {\partial w \over \partial a} = 0
|
||||
$$
|
||||
|
||||
$$
|
||||
\delta_b = {\partial J \over \partial b} = {\partial J \over \partial w} {\partial w \over \partial b} = \delta_w \cdot 1 = \delta_w
|
||||
$$
|
||||
|
||||
$$
|
||||
\delta_e = {\partial J \over \partial e} = {\partial J \over \partial w} {\partial w \over \partial e} = 0
|
||||
$$
|
||||
|
||||
$$
|
||||
\delta_f = {\partial J \over \partial f} = {\partial J \over \partial w} {\partial w \over \partial f} = 0
|
||||
$$
|
||||
|
||||
#### Mean Pooling
|
||||
|
||||
正向公式:
|
||||
|
||||
$$w = \frac{1}{4}(a+b+e+f)$$
|
||||
|
||||
反向公式(假设Layer-1中的最大值是b):
|
||||
|
||||
$$
|
||||
{\partial w \over \partial a} = \frac{1}{4}, \quad {\partial w \over \partial b} = \frac{1}{4}
|
||||
$$
|
||||
|
||||
$$
|
||||
{\partial w \over \partial e} = \frac{1}{4}, \quad {\partial w \over \partial f} = \frac{1}{4}
|
||||
$$
|
||||
|
||||
因为a,b,e,f对w都有贡献,所以偏导数都为1:
|
||||
|
||||
$$
|
||||
\delta_a = {\partial J \over \partial a} = {\partial J \over \partial w} {\partial w \over \partial a} = \frac{1}{4}\delta_w
|
||||
$$
|
||||
|
||||
$$
|
||||
\delta_b = {\partial J \over \partial b} = {\partial J \over \partial w} {\partial w \over \partial b} = \frac{1}{4}\delta_w
|
||||
$$
|
||||
|
||||
$$
|
||||
\delta_e = {\partial J \over \partial e} = {\partial J \over \partial w} {\partial w \over \partial e} = \frac{1}{4}\delta_w
|
||||
$$
|
||||
|
||||
$$
|
||||
\delta_f = {\partial J \over \partial f} = {\partial J \over \partial w} {\partial w \over \partial f} = \frac{1}{4}\delta_w
|
||||
$$
|
||||
|
||||
无论是max pooling还是mean pooling,都没有要学习的参数,所以,在卷积网络的训练中,池化层需要做的只是把误差项向后传递,不需要计算任何梯度。
|
||||
|
||||
### 17.5.4 实现方法1
|
||||
|
||||
按照标准公式来实现池化的正向和反向代码。
|
||||
|
||||
```Python
|
||||
class PoolingLayer(CLayer):
|
||||
def forward_numba(self, x, train=True):
|
||||
......
|
||||
|
||||
def backward_numba(self, delta_in, layer_idx):
|
||||
......
|
||||
```
|
||||
|
||||
有了前面的经验,这次我们直接把前向和反向函数用numba方式来实现,并在前面加上@nb.jit修饰符:
|
||||
```Python
|
||||
@nb.jit(nopython=True)
|
||||
def jit_maxpool_forward(...):
|
||||
...
|
||||
return z
|
||||
|
||||
@nb.jit(nopython=True)
|
||||
def jit_maxpool_backward(...):
|
||||
...
|
||||
return delta_out
|
||||
```
|
||||
|
||||
### 17.5.5 实现方法2
|
||||
|
||||
池化也有类似与卷积优化的方法来计算,在图17-36中,我们假设大写字母为池子中的最大元素,并且用max_pool方式。
|
||||
|
||||
<img src="./img/17/img2col_pool.png" />
|
||||
|
||||
图17-36 池化层的img2col实现
|
||||
|
||||
原始数据先做img2col变换,然后做一次np.max(axis=1)的max计算,会大大增加速度,然后把结果reshape成正确的矩阵即可。做一次大矩阵的max计算,比做4次小矩阵计算要快很多。
|
||||
|
||||
```Python
|
||||
class PoolingLayer(CLayer):
|
||||
def forward_img2col(self, x, train=True):
|
||||
......
|
||||
|
||||
def backward_col2img(self, delta_in, layer_idx):
|
||||
......
|
||||
```
|
||||
|
||||
### 17.5.6 性能测试
|
||||
|
||||
下面我们要比较一下以上两种实现方式的性能,来最终决定使用哪一种。
|
||||
|
||||
对同样的一批64个样本,分别用两种方法做5000次的前向和反向计算,得到的结果:
|
||||
|
||||
```
|
||||
Elapsed of numba: 17.537396907806396
|
||||
Elapsed of img2col: 22.51519775390625
|
||||
forward: True
|
||||
backward: True
|
||||
```
|
||||
|
||||
numba方法用了17秒,img2col方法用了22秒。并且两种方法的返回矩阵值是一样的,说明代码实现正确。
|
||||
|
||||
### 代码位置
|
||||
|
||||
|
|
|
@ -3,6 +3,8 @@
|
|||
|
||||
## 18.1 实现颜色分类
|
||||
|
||||
以下为本小节目录,详情请参阅《智能之门》正版图书,高等教育出版社。
|
||||
|
||||
### 18.1.1 提出问题
|
||||
|
||||
大家知道卷积神经网络可以在图像分类上发挥作用,而一般的图像都是彩色的,也就是说卷积神经网络应该可以判别颜色的。这一节中我们来测试一下颜色分类问题,也就是说,不管几何图形是什么样子的,只针对颜色进行分类。
|
||||
|
@ -34,293 +36,15 @@
|
|||
|
||||
### 18.1.2 用前馈神经网络解决问题
|
||||
|
||||
#### 数据处理
|
||||
|
||||
由于输入图片是三通道的彩色图片,我们先把它转换成灰度图,
|
||||
|
||||
```Python
|
||||
class GeometryDataReader(DataReader_2_0):
|
||||
def ConvertToGray(self, data):
|
||||
(N,C,H,W) = data.shape
|
||||
new_data = np.empty((N,H*W))
|
||||
if C == 3: # color
|
||||
for i in range(N):
|
||||
new_data[i] = np.dot(
|
||||
[0.299,0.587,0.114],
|
||||
data[i].reshape(3,-1)).reshape(1,784)
|
||||
elif C == 1: # gray
|
||||
new_data[i] = data[i,0].reshape(1,784)
|
||||
#end if
|
||||
return new_data
|
||||
```
|
||||
|
||||
向量[0.299,0.587,0.114]的作用是,把三通道的彩色图片的RGB值与此向量相乘,得到灰度图,三个因子相加等于1,这样如果原来是[255,255,255]的话,最后的灰度图的值还是255。如果是[255,255,0]的话,最后的结果是:
|
||||
|
||||
$$
|
||||
\begin{aligned}
|
||||
Y &= 0.299 \cdot R + 0.586 \cdot G + 0.114 \cdot B \\
|
||||
&= 0.299 \cdot 255 + 0.586 \cdot 255 + 0.114 \cdot 0 \\
|
||||
&=225.675
|
||||
\end{aligned}
|
||||
\tag{1}
|
||||
$$
|
||||
|
||||
也就是说粉色的数值本来是(255,255,0),变成了单一的值225.675。六种颜色中的每一种都会有不同的值,所以即使是在灰度图中,也会保留部分“彩色”信息,当然会丢失一些信息。这从公式1中很容易看出来,假设$B=0$,不同组合的$R、G$的值有可能得到相同的最终结果,因此会丢失彩色信息。
|
||||
|
||||
在转换成灰度图后,立刻用reshape(1,784)把它转变成矢量,该矢量就是每个样本的784维的特征值。
|
||||
|
||||
#### 搭建模型
|
||||
|
||||
我们搭建的前馈神经网络模型如下:
|
||||
|
||||
```Python
|
||||
def dnn_model():
|
||||
num_output = 6
|
||||
max_epoch = 100
|
||||
batch_size = 16
|
||||
learning_rate = 0.01
|
||||
params = HyperParameters_4_2(
|
||||
learning_rate, max_epoch, batch_size,
|
||||
net_type=NetType.MultipleClassifier,
|
||||
init_method=InitialMethod.MSRA,
|
||||
optimizer_name=OptimizerName.SGD)
|
||||
|
||||
net = NeuralNet_4_2(params, "color_dnn")
|
||||
|
||||
f1 = FcLayer_2_0(784, 128, params)
|
||||
net.add_layer(f1, "f1")
|
||||
r1 = ActivationLayer(Relu())
|
||||
net.add_layer(r1, "relu1")
|
||||
|
||||
f2 = FcLayer_2_0(f1.output_size, 64, params)
|
||||
net.add_layer(f2, "f2")
|
||||
r2 = ActivationLayer(Relu())
|
||||
net.add_layer(r2, "relu2")
|
||||
|
||||
f3 = FcLayer_2_0(f2.output_size, num_output, params)
|
||||
net.add_layer(f3, "f3")
|
||||
s3 = ClassificationLayer(Softmax())
|
||||
net.add_layer(s3, "s3")
|
||||
|
||||
return net
|
||||
```
|
||||
|
||||
这就是一个普通的三层网络,两个隐层,神经元数量分别是128和64,一个输出层,最后接一个6分类Softmax。
|
||||
|
||||
#### 运行结果
|
||||
|
||||
训练100个epoch后,得到如下损失函数图。
|
||||
|
||||
<img src="./img/18/color_dnn_loss.png" />
|
||||
|
||||
图18-15 训练过程中的损失函数值和准确度变化曲线
|
||||
|
||||
从损失函数曲线可以看到,此网络已经有些轻微的过拟合了,如果重复多次运行训练过程,会得到75%到85%之间的一个准确度值,并不是非常稳定,但偏差也不会太大,这与样本的噪音有很大关系,比如一条很细的红色直线,可能会给训练带来一些不确定因素。
|
||||
|
||||
最后我们考察一下该模型在测试集上的表现:
|
||||
|
||||
```
|
||||
......
|
||||
epoch=99, total_iteration=28199
|
||||
loss_train=0.005832, accuracy_train=1.000000
|
||||
loss_valid=0.593325, accuracy_valid=0.804000
|
||||
save parameters
|
||||
time used: 30.822062015533447
|
||||
testing...
|
||||
0.816
|
||||
```
|
||||
|
||||
在图18-16的可视化结果,一共64张图,是测试集中1000个样本的前64个样本,每张图上方的标签是预测的结果。
|
||||
|
||||
<img src="./img/18/color_dnn_result.png" ch="500" />
|
||||
|
||||
图18-16 可视化结果
|
||||
|
||||
可以看到有很多直线的颜色被识别错了,比如最后一行的第1、3、5、6列,颜色错误。另外有一些大色块也没有识别对,比如第3行最后一列和第4行的头尾两个,都是大色块识别错误。也就是说,对两类形状上的颜色判断不准:
|
||||
|
||||
- 很细的线
|
||||
- 很大的色块
|
||||
|
||||
这是什么原因呢?笔者分析:
|
||||
|
||||
1. 针对细直线,由于带颜色的像素点的数量非常少,被拆成向量后,这些像素点就会在1x784的矢量中彼此相距很远,特征不明显,很容易被判别成噪音;
|
||||
2. 针对大色块,由于带颜色的像素点的数量非常多,即使被拆成向量,也会占据很大的部分,这样特征点与背景点的比例失衡,导致无法判断出到底哪个是特征点。
|
||||
|
||||
笔者认为以上两点是前馈神经网络在训练上的不稳定,以及最后准确度不高的主要原因。
|
||||
|
||||
当然有兴趣的读者也可以保留输入样本的三个彩色通道信息,把一个样本数据变成1x3x784=2352的向量进行试验,看看是不是可以提高准确率。
|
||||
|
||||
### 18.1.3 用卷积神经网络解决问题
|
||||
|
||||
下面我们看看卷积神经网络的表现。我们直接使用三通道的彩色图片,不需要再做数据转换了。
|
||||
|
||||
#### 搭建模型
|
||||
|
||||
```Python
|
||||
def cnn_model():
|
||||
num_output = 6
|
||||
max_epoch = 20
|
||||
batch_size = 16
|
||||
learning_rate = 0.1
|
||||
params = HyperParameters_4_2(
|
||||
learning_rate, max_epoch, batch_size,
|
||||
net_type=NetType.MultipleClassifier,
|
||||
init_method=InitialMethod.MSRA,
|
||||
optimizer_name=OptimizerName.SGD)
|
||||
|
||||
net = NeuralNet_4_2(params, "color_conv")
|
||||
|
||||
c1 = ConvLayer((3,28,28), (2,1,1), (1,0), params)
|
||||
net.add_layer(c1, "c1")
|
||||
r1 = ActivationLayer(Relu())
|
||||
net.add_layer(r1, "relu1")
|
||||
p1 = PoolingLayer(c1.output_shape, (2,2), 2, PoolingTypes.MAX)
|
||||
net.add_layer(p1, "p1")
|
||||
|
||||
c2 = ConvLayer(p1.output_shape, (3,3,3), (1,0), params)
|
||||
net.add_layer(c2, "c2")
|
||||
r2 = ActivationLayer(Relu())
|
||||
net.add_layer(r2, "relu2")
|
||||
p2 = PoolingLayer(c2.output_shape, (2,2), 2, PoolingTypes.MAX)
|
||||
net.add_layer(p2, "p2")
|
||||
|
||||
params.learning_rate = 0.1
|
||||
|
||||
f3 = FcLayer_2_0(p2.output_size, 32, params)
|
||||
net.add_layer(f3, "f3")
|
||||
bn3 = BnLayer(f3.output_size)
|
||||
net.add_layer(bn3, "bn3")
|
||||
r3 = ActivationLayer(Relu())
|
||||
net.add_layer(r3, "relu3")
|
||||
|
||||
f4 = FcLayer_2_0(f3.output_size, num_output, params)
|
||||
net.add_layer(f4, "f4")
|
||||
s4 = ClassificationLayer(Softmax())
|
||||
net.add_layer(s4, "s4")
|
||||
|
||||
return net
|
||||
```
|
||||
|
||||
表18-1展示了在这个模型中各层的作用和参数。
|
||||
|
||||
表18-1 模型各层的参数
|
||||
|
||||
|ID|类型|参数|输入尺寸|输出尺寸|
|
||||
|---|---|---|---|---|
|
||||
|1|卷积|2x1x1, S=1|3x28x28|2x28x28|
|
||||
|2|激活|Relu|2x28x28|2x28x28|
|
||||
|3|池化|2x2, S=2, Max|2x14x14|2x14x14|
|
||||
|4|卷积|3x3x3, S=1|2x14x14|3x12x12|
|
||||
|5|激活|Relu|3x12x12|3x12x12|
|
||||
|6|池化|2x2, S=2, Max|3x12x12|3x6x6|
|
||||
|7|全连接|32|108|32|
|
||||
|8|归一化||32|32|
|
||||
|9|激活|Relu|32|32|
|
||||
|10|全连接|6|32|6|
|
||||
|11|分类|Softmax|6|6|
|
||||
|
||||
为什么第一梯队的卷积用2个卷积核,而第二梯队的卷积核用3个呢?只是经过调参试验的结果,是最小的配置。如果使用更多的卷积核当然可以完成问题,但是如果使用更少的卷积核,网络能力就不够了,不能收敛。
|
||||
|
||||
#### 运行结果
|
||||
|
||||
经过20个epoch的训练后,得到的结果如图18-17。
|
||||
|
||||
<img src="./img/18/color_cnn_loss.png" />
|
||||
|
||||
图18-17 训练过程中的损失函数值和准确度变化曲线
|
||||
|
||||
以下是打印输出的最后几行:
|
||||
|
||||
```
|
||||
......
|
||||
epoch=19, total_iteration=5639
|
||||
loss_train=0.005293, accuracy_train=1.000000
|
||||
loss_valid=0.106723, accuracy_valid=0.968000
|
||||
save parameters
|
||||
time used: 17.295073986053467
|
||||
testing...
|
||||
0.963
|
||||
```
|
||||
|
||||
可以看到我们在测试集上得到了96.3%的准确度,比前馈神经网络模型要高出很多,这也证明了卷积神经网络在图像识别上的能力。
|
||||
|
||||
图18-18是测试集中前64个测试样本的预测结果。
|
||||
|
||||
<img src="./img/18/color_cnn_result.png" ch="500" />
|
||||
|
||||
图18-18 测试结果
|
||||
|
||||
在这一批的样本中,只有左下角的一个绿色直线被预测成蓝色了,其它的没发生错误。
|
||||
|
||||
### 18.1.4 1x1卷积
|
||||
|
||||
读者可能还记得在GoogLeNet的Inception模块中,有1x1的卷积核。这初看起来是一个非常奇怪的做法,因为1x1的卷积核基本上失去了卷积的作用,并没有建立在同一个通道上的相邻像素之间的相关性。
|
||||
|
||||
在本例中,为了识别颜色,我们也使用了1x1的卷积核,并且能够完成颜色分类的任务,这是为什么呢?
|
||||
|
||||
我们以三通道的数据举例。
|
||||
|
||||
<img src="./img/18/OneByOne.png" ch="500" />
|
||||
|
||||
图18-19 1x1卷积核的工作原理
|
||||
|
||||
假设有一个三通道的1x1的卷积核,其值为[1,2,-1],则相当于把每个通道的同一位置的像素值乘以卷积核,然后把结果相加,作为输出通道的同一位置的像素值。以左上角的像素点为例:
|
||||
|
||||
$$
|
||||
1 \times 1 + 1 \times 2 + 1 \times (-1)=2
|
||||
$$
|
||||
|
||||
相当于把上图拆开成9个样本,其值为:
|
||||
|
||||
```
|
||||
[1,1,1] # 左上角点
|
||||
[3,3,0] # 中上点
|
||||
[0,0,0] # 右上角点
|
||||
[2,0,0] # 左中点
|
||||
[0,1,1] # 中点
|
||||
[4,2,1] # 右中点
|
||||
[1,1,1] # 左下角点
|
||||
[2,1,1] # 下中点
|
||||
[0,0,0] # 右下角点
|
||||
```
|
||||
|
||||
上述值排成一个9行3列的矩阵,然后与一个3行1列的向量$(1,2,-1)^T$相乘,得到9行1列的向量,然后再转换成3x3的矩阵。当然在实际过程中,这个1x1的卷积核的数值是学习出来的,而不是人为指定的。
|
||||
|
||||
这样做可以达到两个目的:
|
||||
|
||||
1. 跨通道信息整合
|
||||
2. 降维以减少学习参数
|
||||
|
||||
所以1x1的卷积核关注的是不同通道的相同位置的像素之间的相关性,而不是同一通道内的像素的相关性,在本例中,意味着它关心的彩色通道信息,通过不同的卷积核,把彩色通道信息转变成另外一种表达方式,在保留原始信息的同时,还实现了降维。
|
||||
|
||||
在本例中,第一层卷积如果使用3个卷积核,输出尺寸是3x28x28,和输入尺寸一样,达不到降维的作用。所以,一般情况下,会使用小于输入通道数的卷积核数量,比如输入通道为3,则使用2个或1个卷积核。在上例中,如果使用2个卷积核,则输出两张9x9的特征图,这样才能达到降维的目的。如果想升维,那么使用4个以上的卷积核就可以了。
|
||||
|
||||
### 18.1.5 颜色分类可视化解释
|
||||
|
||||
在这里笔者根据自己的理解,解释一下针对这个颜色分类问题,卷积神经网络是如何工作的。
|
||||
|
||||
<img src='./img/18/color_cnn_visualization.png'/>
|
||||
|
||||
图18-20 颜色分类问题的可视化解释
|
||||
|
||||
如图18-20所示:
|
||||
|
||||
1. 第一行是原始彩色图片,三通道28x28,特意挑出来都是矩形的6种颜色。
|
||||
|
||||
2. 第二行是第一卷积组合梯队的第1个1x1的卷积核在原始图片上的卷积结果。由于是1x1的卷积核,相当于用3个浮点数分别乘以三通道的颜色值所得到和,只要是最后的值不一样就可以了,因为对于神经网络来说,没有颜色这个概念,只有数值。从人的角度来看,6张图的前景颜色是不同的(因为原始图的前景色是6种不同颜色)。
|
||||
|
||||
3. 第三行是第一卷积组合梯队的第2个1x1的卷积核在原始图片上的卷积结果。与2相似,只不过3个浮点数的数值不同而已,也是得到6张前景色不同的图。
|
||||
|
||||
4. 第四行是第二卷积组合梯队的三个卷积核的卷积结果图,把三个特征图当作RGB通道后所生成的彩色图。单独看三个特征图的话,人类是无法理解的,所以我们把三个通道变成假的彩色图,仍然可以做到6个样本不同色,但是出现了一些边框,可以认为是卷积层从颜色上抽取出的“特征”,也就是说卷积网络“看”到了我们人类不能理解的东西。
|
||||
|
||||
5. 第五行是第二卷积组合梯队的激活函数结果,和原始图片相差很大。
|
||||
|
||||
如果用人类的视觉神经系统做类比,两个1x1的卷积核可以理解为两只眼睛上的视网膜上的视觉神经细胞,把彩色信息转变成神经电信号传入大脑的过程。最后由全连接层做分类,相当于大脑中的视觉知识体系。
|
||||
|
||||
回到神经网络的问题上,只要ReLU的输出结果中仍然含有“颜色”信息(用假彩色图可以证明这一点),并且针对原始图像中的不同的颜色,会生成不同的假彩色图,最后的全连接网络就可以有分辨的能力。
|
||||
|
||||
举例来说,从图18-20看,第一行的红色到了第五行变成了黑色,绿色变成了淡绿色,等等,是一一对应的关系。如果红色和绿色都变成了黑色,那么将分辨不出区别来。
|
||||
|
||||
### 代码位置
|
||||
|
||||
ch18, Level1
|
||||
|
|
|
@ -3,6 +3,8 @@
|
|||
|
||||
## 18.2 实现几何图形分类
|
||||
|
||||
以下为本小节目录,详情请参阅《智能之门》正版图书,高等教育出版社。
|
||||
|
||||
### 18.2.1 提出问题
|
||||
|
||||
有一种儿童玩具:在一个平板上面有三种形状的洞:圆形、三角形、正方形,让小朋友们拿着这三种形状的积木从对应的洞中穿过那个平板就算成功。如果形状不对是穿不过去的,比如一个圆形的积木无法穿过一个方形的洞。这就要求儿童先学会识别几何形状,学会匹配,然后手眼脑配合才能成功。
|
||||
|
@ -19,181 +21,10 @@
|
|||
|
||||
### 18.2.2 用前馈神经网络解决问题
|
||||
|
||||
我们下面要考验一下神经网络的能力。我们先用前面学过的全连接网络来解决这个问题,搭建一个三层的网络如下:
|
||||
|
||||
```Python
|
||||
def dnn_model():
|
||||
num_output = 5
|
||||
max_epoch = 50
|
||||
batch_size = 16
|
||||
learning_rate = 0.1
|
||||
params = HyperParameters_4_2(
|
||||
learning_rate, max_epoch, batch_size,
|
||||
net_type=NetType.MultipleClassifier,
|
||||
init_method=InitialMethod.MSRA,
|
||||
optimizer_name=OptimizerName.SGD)
|
||||
|
||||
net = NeuralNet_4_2(params, "pic_dnn")
|
||||
|
||||
f1 = FcLayer_2_0(784, 128, params)
|
||||
net.add_layer(f1, "f1")
|
||||
r1 = ActivationLayer(Relu())
|
||||
net.add_layer(r1, "relu1")
|
||||
|
||||
f2 = FcLayer_2_0(f1.output_size, 64, params)
|
||||
net.add_layer(f2, "f2")
|
||||
r2 = ActivationLayer(Relu())
|
||||
net.add_layer(r2, "relu2")
|
||||
|
||||
f3 = FcLayer_2_0(f2.output_size, num_output, params)
|
||||
net.add_layer(f3, "f3")
|
||||
s3 = ClassificationLayer(Softmax())
|
||||
net.add_layer(s3, "s3")
|
||||
|
||||
return net
|
||||
```
|
||||
|
||||
样本数据为28x28的灰度图,所以我们要把它展开成1x784的向量,第一层用128个神经元,第二层用64个神经元,输出层5个神经元接Softmax分类函数。
|
||||
|
||||
最后可以得到如下训练结果。
|
||||
|
||||
<img src="./img/18/shape_dnn_loss.png" />
|
||||
|
||||
图18-22 训练过程中损失函数值和准确度的变化
|
||||
|
||||
在测试集上得到的准确度是89.8%,这已经超出笔者的预期了,本来猜测准确度会小于80%。有兴趣的读者可以再精调一下这个前馈神经网络网络,看看是否可以得到更高的准确度。
|
||||
|
||||
### 18.2.3 用卷积神经网络解决问题
|
||||
|
||||
下面我们来看看卷积神经网络能不能完成这个工作。首先搭建网络模型如下:
|
||||
|
||||
```Python
|
||||
def cnn_model():
|
||||
num_output = 5
|
||||
max_epoch = 50
|
||||
batch_size = 16
|
||||
learning_rate = 0.1
|
||||
params = HyperParameters_4_2(
|
||||
learning_rate, max_epoch, batch_size,
|
||||
net_type=NetType.MultipleClassifier,
|
||||
init_method=InitialMethod.MSRA,
|
||||
optimizer_name=OptimizerName.SGD)
|
||||
|
||||
net = NeuralNet_4_2(params, "shape_cnn")
|
||||
|
||||
c1 = ConvLayer((1,28,28), (8,3,3), (1,1), params)
|
||||
net.add_layer(c1, "c1")
|
||||
r1 = ActivationLayer(Relu())
|
||||
net.add_layer(r1, "relu1")
|
||||
p1 = PoolingLayer(c1.output_shape, (2,2), 2, PoolingTypes.MAX)
|
||||
net.add_layer(p1, "p1")
|
||||
|
||||
c2 = ConvLayer(p1.output_shape, (16,3,3), (1,0), params)
|
||||
net.add_layer(c2, "c2")
|
||||
r2 = ActivationLayer(Relu())
|
||||
net.add_layer(r2, "relu2")
|
||||
p2 = PoolingLayer(c2.output_shape, (2,2), 2, PoolingTypes.MAX)
|
||||
net.add_layer(p2, "p2")
|
||||
|
||||
params.learning_rate = 0.1
|
||||
|
||||
f3 = FcLayer_2_0(p2.output_size, 32, params)
|
||||
net.add_layer(f3, "f3")
|
||||
bn3 = BnLayer(f3.output_size)
|
||||
net.add_layer(bn3, "bn3")
|
||||
r3 = ActivationLayer(Relu())
|
||||
net.add_layer(r3, "relu3")
|
||||
|
||||
f4 = FcLayer_2_0(f3.output_size, num_output, params)
|
||||
net.add_layer(f4, "f4")
|
||||
s4 = ClassificationLayer(Softmax())
|
||||
net.add_layer(s4, "s4")
|
||||
|
||||
return net
|
||||
```
|
||||
表18-2展示了模型中各层的作用和参数。
|
||||
|
||||
表18-2 模型各层的作用和参数
|
||||
|
||||
|ID|类型|参数|输入尺寸|输出尺寸|
|
||||
|---|---|---|---|---|
|
||||
|1|卷积|8x3x3, S=1,P=1|1x28x28|8x28x28|
|
||||
|2|激活|Relu|8x28x28|8x28x28|
|
||||
|3|池化|2x2, S=2, Max|8x28x28|8x14x14|
|
||||
|4|卷积|16x3x3, S=1|8x14x14|16x12x12|
|
||||
|5|激活|Relu|16x12x12|16x12x12|
|
||||
|6|池化|2x2, S=2, Max|16x6x6|16x6x6|
|
||||
|7|全连接|32|576|32|
|
||||
|8|归一化||32|32|
|
||||
|9|激活|Relu|32|32|
|
||||
|10|全连接|5|32|5|
|
||||
|11|分类|Softmax|5|5|
|
||||
|
||||
经过50个epoch的训练后,我们得到的结果如图18-23。
|
||||
|
||||
<img src="./img/18/shape_cnn_loss.png" />
|
||||
|
||||
图18-23 训练过程中损失函数值和准确度的变化
|
||||
|
||||
以下是打印输出的最后几行:
|
||||
|
||||
```
|
||||
......
|
||||
epoch=49, total_iteration=14099
|
||||
loss_train=0.002093, accuracy_train=1.000000
|
||||
loss_valid=0.163053, accuracy_valid=0.944000
|
||||
time used: 259.32207012176514
|
||||
testing...
|
||||
0.935
|
||||
load parameters
|
||||
0.96
|
||||
```
|
||||
|
||||
可以看到我们在测试集上得到了96%的准确度,比前馈神经网络模型要高出很多,这也证明了卷积神经网络在图像识别上的能力。
|
||||
|
||||
图18-24是部分测试集中的测试样本的预测结果。
|
||||
|
||||
<img src='./img/18/shape_cnn_result.png'/>
|
||||
|
||||
图18-24 测试结果
|
||||
|
||||
绝大部分样本预测是正确的,只有最后一个样本,看上去应该是一个很扁的三角形,被预测成了菱形。
|
||||
|
||||
### 18.2.4 形状分类可视化解释
|
||||
|
||||
<img src='./img/18/shape_cnn_visualization.png'/>
|
||||
|
||||
图18-25 可视化解释
|
||||
|
||||
参看图18-25,表18-3解释了8个卷积核的作用。
|
||||
|
||||
表18-3 8个卷积核的作用
|
||||
|
||||
|卷积核序号|作用|直线|三角形|菱形|矩形|圆形|
|
||||
|:--:|---|:--:|:--:|:--:|:--:|:--:|
|
||||
|1|左侧边缘|0|1|0|1|1|
|
||||
|2|大色块区域|0|1|1|1|1|
|
||||
|3|左上侧边缘|0|1|1|0|1|
|
||||
|4|45度短边|1|1|1|0|1|
|
||||
|5|右侧边缘、上横边|0|0|0|1|1|
|
||||
|6|左上、右上、右下|0|1|1|0|1|
|
||||
|7|左边框和右下角|0|0|0|1|1|
|
||||
|8|左上和右下,及背景|0|0|1|0|1|
|
||||
|
||||
表18-3中,左侧为卷积核的作用,右侧为某个特征对于5种形状的判别力度,0表示该特征无法找到,1表示可以找到该特征。
|
||||
|
||||
1. 比如第一个卷积核,其作用为判断是否有左侧边缘,那么第一行的数据为[0,1,0,1,1],表示对直线和菱形来说,没有左侧边缘特征,而对于三角形、矩形、圆形来说,有左侧边缘特征。这样的话,就可以根据这个特征把5种形状分为两类:
|
||||
|
||||
- A类有左侧边缘特征:三角形、矩形、圆形
|
||||
- B类无左侧边缘特征:直线、菱形
|
||||
|
||||
2. 再看第二个卷积核,是判断是否有大色块区域的,只有直线没有该特征,其它4种形状都有。那么看第1个特征的B类种,包括直线、菱形,则第2个特征就可以把直线和菱形分开了。
|
||||
|
||||
3. 然后我们只关注A类形状,看第三个卷积核,判断是否有左上侧边缘,对于三角形、矩形、圆形的取值为[1,0,1],即矩形没有左上侧边缘,这样就可以把矩形从A类中分出来。
|
||||
|
||||
4. 对于三角形和圆形,卷积核5、7、8都可以给出不同的值,这就可以把二者分开了。
|
||||
|
||||
当然,神经网络可能不是按照我们分析的顺序来判定形状的,这只是其中的一种解释路径,还可以有很多其它种路径的组合,但最终总能够把5种形状分开来。
|
||||
|
||||
### 代码位置
|
||||
|
||||
|
|
|
@ -3,173 +3,16 @@
|
|||
|
||||
## 18.3 实现几何图形及颜色分类
|
||||
|
||||
以下为本小节目录,详情请参阅《智能之门》正版图书,高等教育出版社。
|
||||
|
||||
### 18.3.1 提出问题
|
||||
|
||||
在前两节我们学习了如何按颜色分类和按形状分类几何图形,现在我们自然地想到如果把颜色和图形结合起来,卷积神经网络能不能正确分类呢?
|
||||
|
||||
请看样本数据,如图18-26。
|
||||
|
||||
<img src="./img/18/shape_color_sample.png" ch="500" />
|
||||
|
||||
图18-26 样本数据
|
||||
|
||||
一共有3种形状及3种颜色,如表18-4所示。
|
||||
|
||||
表18-4 样本数据分类和数量
|
||||
|
||||
||红色|蓝色|绿色|
|
||||
|---|---|---|---|
|
||||
|圆形|600:100|600:100|600:100|
|
||||
|矩形|600:100|600:100|600:100|
|
||||
|三角形|600:100|600:100|600:100|
|
||||
|
||||
表中列出了9种样本的训练集和测试集的样本数量比例,都是600:100,
|
||||
|
||||
### 18.3.2 用前馈神经网络解决问题
|
||||
|
||||
我们仍然先使用全连接网络来解决这个问题,搭建一个三层的网络如下:
|
||||
|
||||
```Python
|
||||
ef dnn_model():
|
||||
num_output = 9
|
||||
max_epoch = 50
|
||||
batch_size = 16
|
||||
learning_rate = 0.01
|
||||
params = HyperParameters_4_2(
|
||||
learning_rate, max_epoch, batch_size,
|
||||
net_type=NetType.MultipleClassifier,
|
||||
init_method=InitialMethod.MSRA,
|
||||
optimizer_name=OptimizerName.Momentum)
|
||||
|
||||
net = NeuralNet_4_2(params, "color_shape_dnn")
|
||||
|
||||
f1 = FcLayer_2_0(784, 128, params)
|
||||
net.add_layer(f1, "f1")
|
||||
r1 = ActivationLayer(Relu())
|
||||
net.add_layer(r1, "relu1")
|
||||
|
||||
f2 = FcLayer_2_0(f1.output_size, 64, params)
|
||||
net.add_layer(f2, "f2")
|
||||
r2 = ActivationLayer(Relu())
|
||||
net.add_layer(r2, "relu2")
|
||||
|
||||
f3 = FcLayer_2_0(f2.output_size, num_output, params)
|
||||
net.add_layer(f3, "f3")
|
||||
s3 = ClassificationLayer(Softmax())
|
||||
net.add_layer(s3, "s3")
|
||||
|
||||
return net
|
||||
```
|
||||
|
||||
样本数据为3x28x28的彩色图,所以我们要把它转换成灰度图,然后再展开成1x784的向量,第一层用128个神经元,第二层用64个神经元,输出层用9个神经元接Softmax分类函数。
|
||||
|
||||
训练50个epoch后可以得到如下如图18-27所示的训练结果。
|
||||
|
||||
<img src="./img/18/shape_color_dnn_loss.png" />
|
||||
|
||||
图18-27 训练过程中损失函数值和准确度的变化
|
||||
|
||||
```
|
||||
......
|
||||
epoch=49, total_iteration=15199
|
||||
loss_train=0.003370, accuracy_train=1.000000
|
||||
loss_valid=0.510589, accuracy_valid=0.883333
|
||||
time used: 25.34346342086792
|
||||
testing...
|
||||
0.9011111111111111
|
||||
load parameters
|
||||
0.8988888888888888
|
||||
```
|
||||
|
||||
在测试集上得到的准确度是89%,这已经超出笔者的预期了,本来猜测准确度会小于80%。有兴趣的读者可以再精调一下这个前馈神经网络网络,看看是否可以得到更高的准确度。
|
||||
|
||||
图18-28是部分测试集中的测试样本的预测结果。
|
||||
|
||||
<img src="./img/18/shape_color_dnn_result.png" ch="500" />
|
||||
|
||||
图18-28 测试结果
|
||||
|
||||
绝大部分样本预测是正确的,但是第3行第2列的样本,应该是green-rect,被预测成green-circle;最后两行的两个green-tri也被预测错了形状,颜色并没有错。
|
||||
|
||||
### 18.3.3 用卷积神经网络解决问题
|
||||
|
||||
下面我们来看看卷积神经网络能不能完成这个工作。首先搭建网络模型如下:
|
||||
|
||||
```Python
|
||||
def cnn_model():
|
||||
num_output = 9
|
||||
max_epoch = 20
|
||||
batch_size = 16
|
||||
learning_rate = 0.1
|
||||
params = HyperParameters_4_2(
|
||||
learning_rate, max_epoch, batch_size,
|
||||
net_type=NetType.MultipleClassifier,
|
||||
init_method=InitialMethod.MSRA,
|
||||
optimizer_name=OptimizerName.SGD)
|
||||
|
||||
net = NeuralNet_4_2(params, "shape_color_cnn")
|
||||
|
||||
c1 = ConvLayer((3,28,28), (8,3,3), (1,1), params)
|
||||
net.add_layer(c1, "c1")
|
||||
r1 = ActivationLayer(Relu())
|
||||
net.add_layer(r1, "relu1")
|
||||
p1 = PoolingLayer(c1.output_shape, (2,2), 2, PoolingTypes.MAX)
|
||||
net.add_layer(p1, "p1")
|
||||
|
||||
c2 = ConvLayer(p1.output_shape, (16,3,3), (1,0), params)
|
||||
net.add_layer(c2, "c2")
|
||||
r2 = ActivationLayer(Relu())
|
||||
net.add_layer(r2, "relu2")
|
||||
p2 = PoolingLayer(c2.output_shape, (2,2), 2, PoolingTypes.MAX)
|
||||
net.add_layer(p2, "p2")
|
||||
|
||||
params.learning_rate = 0.1
|
||||
|
||||
f3 = FcLayer_2_0(p2.output_size, 32, params)
|
||||
net.add_layer(f3, "f3")
|
||||
bn3 = BnLayer(f3.output_size)
|
||||
net.add_layer(bn3, "bn3")
|
||||
r3 = ActivationLayer(Relu())
|
||||
net.add_layer(r3, "relu3")
|
||||
|
||||
f4 = FcLayer_2_0(f3.output_size, num_output, params)
|
||||
net.add_layer(f4, "f4")
|
||||
s4 = ClassificationLayer(Softmax())
|
||||
net.add_layer(s4, "s4")
|
||||
|
||||
return net
|
||||
```
|
||||
|
||||
经过20个epoch的训练后,我们得到的结果如图18-29。
|
||||
|
||||
<img src="./img/18/shape_color_cnn_loss.png" />
|
||||
|
||||
图18-29 训练过程中损失函数值和准确度的变化
|
||||
|
||||
以下是打印输出的最后几行:
|
||||
|
||||
```
|
||||
......
|
||||
epoch=19, total_iteration=6079
|
||||
loss_train=0.005184, accuracy_train=1.000000
|
||||
loss_valid=0.118708, accuracy_valid=0.957407
|
||||
time used: 131.77996039390564
|
||||
testing...
|
||||
0.97
|
||||
load parameters
|
||||
0.97
|
||||
```
|
||||
|
||||
可以看到我们在测试集上得到了97%的准确度,比DNN模型要高出很多,这也证明了卷积神经网络在图像识别上的能力。
|
||||
|
||||
图18-30是部分测试集中的测试样本的预测结果。
|
||||
|
||||
<img src="./img/18/shape_color_cnn_result.png" ch="500" />
|
||||
|
||||
图18-30 测试结果
|
||||
|
||||
绝大部分样本预测是正确的,只有最后一行第4个样本,本来是green-triangle,被预测成green-circle。
|
||||
|
||||
### 代码位置
|
||||
|
||||
ch18, Level3_ColorAndShapeConvNet.py
|
||||
|
|
|
@ -3,171 +3,16 @@
|
|||
|
||||
## 18.4 解决MNIST分类问题
|
||||
|
||||
以下为本小节目录,详情请参阅《智能之门》正版图书,高等教育出版社。
|
||||
|
||||
### 18.4.1 模型搭建
|
||||
|
||||
在12.1中,我们用一个三层的神经网络解决MNIST问题,并得到了97.49%的准确率。当时使用的模型如图18-31。
|
||||
|
||||
<img src="./img/18/nn3.png" ch="500" />
|
||||
|
||||
图18-31 前馈神经网络模型解决MNIST问题
|
||||
|
||||
这一节中,我们将学习如何使用卷积网络来解决MNIST问题。首先搭建模型如图18-32。
|
||||
|
||||
<img src="./img/18/mnist_net.png" />
|
||||
|
||||
图18-32 卷积神经网络模型解决MNIST问题
|
||||
|
||||
表18-5展示了模型中各层的功能和参数。
|
||||
|
||||
表18-5 模型中各层的功能和参数
|
||||
|
||||
|Layer|参数|输入|输出|参数个数|
|
||||
|---|---|---|---|---|
|
||||
|卷积层|8x5x5,s=1|1x28x28|8x24x24|200+8|
|
||||
|激活层|2x2,s=2, max|8x24x24|8x24x24||
|
||||
|池化层|Relu|8x24x24|8x12x12||
|
||||
|卷积层|16x5x5,s=1|8x12x12|16x8x8|400+16|
|
||||
|激活层|Relu|16x8x8|16x8x8||
|
||||
|池化层|2x2, s=2, max|16x8x8|16x4x4||
|
||||
|全连接层|256x32|256|32|8192+32|
|
||||
|批归一化层||32|32||
|
||||
|激活层|Relu|32|32||
|
||||
|全连接层|32x10|32|10|320+10|
|
||||
|分类层|softmax,10|10|10|
|
||||
|
||||
卷积核的大小如何选取呢?大部分卷积神经网络都会用1、3、5、7的方式递增,还要注意在做池化时,应该尽量让输入的矩阵尺寸是偶数,如果不是的话,应该在上一层卷积层加padding,使得卷积的输出结果矩阵的宽和高为偶数。
|
||||
|
||||
### 18.4.2 代码实现
|
||||
|
||||
```Python
|
||||
def model():
|
||||
num_output = 10
|
||||
dataReader = LoadData(num_output)
|
||||
|
||||
max_epoch = 5
|
||||
batch_size = 128
|
||||
learning_rate = 0.1
|
||||
params = HyperParameters_4_2(
|
||||
learning_rate, max_epoch, batch_size,
|
||||
net_type=NetType.MultipleClassifier,
|
||||
init_method=InitialMethod.Xavier,
|
||||
optimizer_name=OptimizerName.Momentum)
|
||||
|
||||
net = NeuralNet_4_2(params, "mnist_conv_test")
|
||||
|
||||
c1 = ConvLayer((1,28,28), (8,5,5), (1,0), params)
|
||||
net.add_layer(c1, "c1")
|
||||
r1 = ActivationLayer(Relu())
|
||||
net.add_layer(r1, "relu1")
|
||||
p1 = PoolingLayer(c1.output_shape, (2,2), 2, PoolingTypes.MAX)
|
||||
net.add_layer(p1, "p1")
|
||||
|
||||
c2 = ConvLayer(p1.output_shape, (16,5,5), (1,0), params)
|
||||
net.add_layer(c2, "23")
|
||||
r2 = ActivationLayer(Relu())
|
||||
net.add_layer(r2, "relu2")
|
||||
p2 = PoolingLayer(c2.output_shape, (2,2), 2, PoolingTypes.MAX)
|
||||
net.add_layer(p2, "p2")
|
||||
|
||||
f3 = FcLayer_2_0(p2.output_size, 32, params)
|
||||
net.add_layer(f3, "f3")
|
||||
bn3 = BnLayer(f3.output_size)
|
||||
net.add_layer(bn3, "bn3")
|
||||
r3 = ActivationLayer(Relu())
|
||||
net.add_layer(r3, "relu3")
|
||||
|
||||
f4 = FcLayer_2_0(f3.output_size, 10, params)
|
||||
net.add_layer(f4, "f2")
|
||||
s4 = ClassificationLayer(Softmax())
|
||||
net.add_layer(s4, "s4")
|
||||
|
||||
net.train(dataReader, checkpoint=0.05, need_test=True)
|
||||
net.ShowLossHistory(XCoordinate.Iteration)
|
||||
```
|
||||
|
||||
### 18.4.3 运行结果
|
||||
|
||||
训练5个epoch后的损失函数值和准确率的历史记录曲线如图18-33。
|
||||
|
||||
<img src="./img/18/mnist_loss.png" />
|
||||
|
||||
图18-33 训练过程中损失函数值和准确度的变化
|
||||
|
||||
打印输出结果如下:
|
||||
|
||||
```
|
||||
...
|
||||
epoch=4, total_iteration=2133
|
||||
loss_train=0.054449, accuracy_train=0.984375
|
||||
loss_valid=0.060550, accuracy_valid=0.982000
|
||||
save parameters
|
||||
time used: 513.3446323871613
|
||||
testing...
|
||||
0.9865
|
||||
```
|
||||
|
||||
最后可以得到98.65%的准确率,比全连接网络要高1个百分点。如果想进一步提高准确率,可以尝试增加卷积层的能力,比如使用更多的卷积核来提取更多的特征。
|
||||
|
||||
### 18.4.4 可视化
|
||||
|
||||
#### 第一组的卷积可视化
|
||||
|
||||
下图按行显示了以下内容:
|
||||
|
||||
1. 卷积核数值
|
||||
2. 卷积核抽象
|
||||
3. 卷积结果
|
||||
4. 激活结果
|
||||
5. 池化结果
|
||||
|
||||
<img src="./img/18/mnist_layer_123_filter.png" ch="500" />
|
||||
|
||||
图18-34 卷积结果可视化
|
||||
|
||||
卷积核是5x5的,一共8个卷积核,所以第一行直接展示了卷积核的数值图形化以后的结果,但是由于色块太大,不容易看清楚其具体的模式,那么第二行的模式是如何抽象出来的呢?
|
||||
|
||||
因为特征是未知的,所以卷积神经网络不可能学习出类似下面的两个矩阵中左侧矩阵的整齐的数值,而很可能是如同右侧的矩阵一样具有很多噪音,但是大致轮廓还是个左上到右下的三角形,只是一些局部点上有一些值的波动。
|
||||
|
||||
```
|
||||
2 2 1 1 0 2 0 1 1 0
|
||||
2 1 1 0 0 2 1 1 2 0
|
||||
1 1 0 -1 -2 0 1 0 -1 -2
|
||||
1 0 -1 -2 -3 1 -1 1 -4 -3
|
||||
0 -1 -2 -3 -4 0 -1 -2 -3 -2
|
||||
```
|
||||
|
||||
如何“看”出一个大概符合某个规律的模板呢?对此,笔者的心得是:
|
||||
|
||||
1. 摘掉眼镜(或者眯起眼睛)看第一行的卷积核的明暗变化模式;
|
||||
2. 也可以用图像处理的办法,把卷积核形成的5x5的点阵做一个模糊处理;
|
||||
3. 结合第三行的卷积结果推想卷积核的行为。
|
||||
|
||||
由此可以得到表18-6的模式。
|
||||
|
||||
表18-6 卷积核的抽象模式
|
||||
|
||||
|卷积核序号|1|2|3|4|5|6|7|8|
|
||||
|:--:|:--:|:--:|:--:|:--:|:--:|:--:|:--:|:--:|
|
||||
|抽象模式|右斜|下|中心|竖中|左下|上|右|左上|
|
||||
|
||||
这些模式实际上就是特征,是卷积网络自己学习出来的,每一个卷积核关注图像的一个特征,比如上部边缘、下部边缘、左下边缘、右下边缘等。这些特征的排列有什么顺序吗?没有。每一次重新训练后,特征可能会变成其它几种组合,顺序也会发生改变,这取决于初始化数值及样本顺序、批大小等等因素。
|
||||
|
||||
当然可以用更高级的图像处理算法,对5x5的图像进行模糊处理,再从中提取模式。
|
||||
|
||||
#### 第二组的卷积可视化
|
||||
|
||||
图18-35是第二组的卷积、激活、池化层的输出结果。
|
||||
|
||||
<img src="./img/18/mnist_layer_456.png" ch="500" />
|
||||
|
||||
图18-35 第二组卷积核、激活、池化的可视化
|
||||
|
||||
- Conv2:由于是在第一层的特征图上卷积后叠加的结果,所以基本不能按照原图理解,但也能大致看出是是一些轮廓抽取的作用;
|
||||
- Relu2:能看出的是如果黑色区域多的话,说明基本没有激活值,此卷积核效果就没用;
|
||||
- Pool2:池化后分化明显的特征图是比较有用的特征,比如3、6、12、15、16;信息太多或者太少的特征图,都用途偏小,比如1、7、10、11。
|
||||
|
||||
### 参考资料
|
||||
|
||||
- http://scs.ryerson.ca/~aharley/vis/conv/
|
||||
|
||||
读者可以在上面这个网站看到MNIST的可视化结果,用鼠标可以改变三维视图的视角。
|
||||
|
|
|
@ -26,126 +26,9 @@ MNIST手写识别数据集,对卷积神经网络来说已经太简单了,于
|
|||
|
||||
### 18.5.2 用前馈神经网络来解决问题
|
||||
|
||||
#### 下载数据
|
||||
|
||||
从 "https://www.kaggle.com/datasets/zalando-research/fashionmnist/download?datasetVersionNumber=4" 下载 archive.zip 文件,解压后拷贝到 "ch18-CNNModel\ExtendedDataReader\data\" 目录下。为了不和已有的 MNIST 数据文件名冲突,可以给每个文件名的前面加上 "fashion-",所以最后一共有四个文件:
|
||||
|
||||
- fashion-t10k-images-idx3-ubyte, 测试图片文件
|
||||
- fashion-t10k-labels-idx1-ubyte, 测试标签文件
|
||||
- fashion-train-images-idx3-ubyte, 训练图片文件
|
||||
- fashion-train-labels-idx1-ubyte, 训练标签文件
|
||||
|
||||
|
||||
#### 搭建模型
|
||||
|
||||
```Python
|
||||
def dnn_model():
|
||||
num_output = 10
|
||||
max_epoch = 10
|
||||
batch_size = 128
|
||||
learning_rate = 0.1
|
||||
params = HyperParameters_4_2(
|
||||
learning_rate, max_epoch, batch_size,
|
||||
net_type=NetType.MultipleClassifier,
|
||||
init_method=InitialMethod.MSRA,
|
||||
optimizer_name=OptimizerName.Momentum)
|
||||
|
||||
net = NeuralNet_4_2(params, "fashion_mnist_dnn")
|
||||
|
||||
f1 = FcLayer_2_0(784, 128, params)
|
||||
net.add_layer(f1, "f1")
|
||||
bn1 = BnLayer(f1.output_size)
|
||||
net.add_layer(bn1, "bn1")
|
||||
r1 = ActivationLayer(Relu())
|
||||
net.add_layer(r1, "relu1")
|
||||
|
||||
f2 = FcLayer_2_0(f1.output_size, 64, params)
|
||||
net.add_layer(f2, "f2")
|
||||
bn2 = BnLayer(f2.output_size)
|
||||
net.add_layer(bn2, "bn2")
|
||||
r2 = ActivationLayer(Relu())
|
||||
net.add_layer(r2, "relu2")
|
||||
|
||||
f3 = FcLayer_2_0(f2.output_size, num_output, params)
|
||||
net.add_layer(f3, "f3")
|
||||
s3 = ClassificationLayer(Softmax())
|
||||
net.add_layer(s3, "s3")
|
||||
|
||||
return net
|
||||
```
|
||||
|
||||
#### 训练结果
|
||||
|
||||
训练10个epoch后得到如图18-37所示曲线,可以看到网络能力已经接近极限了,再训练下去会出现过拟合现象,准确度也不一定能提高。
|
||||
|
||||
<img src="./img/18/FashionMnistLoss_dnn.png" />
|
||||
|
||||
图18-37 训练过程中损失函数值和准确度的变化
|
||||
|
||||
图18-38是在测试集上的预测结果。
|
||||
|
||||
<img src="./img/18/FashionMnistResult_dnn.png" ch="555" />
|
||||
|
||||
图18-38 测试结果
|
||||
|
||||
凡是类别名字前面带*号的,表示预测错误,比如第3行第1列,本来应该是第7类“运动鞋”,却被预测成了“凉鞋”。
|
||||
|
||||
### 18.5.3 用卷积神经网络来解决问题
|
||||
|
||||
#### 搭建模型
|
||||
|
||||
```Python
|
||||
def cnn_model():
|
||||
num_output = 10
|
||||
max_epoch = 10
|
||||
batch_size = 128
|
||||
learning_rate = 0.01
|
||||
params = HyperParameters_4_2(
|
||||
learning_rate, max_epoch, batch_size,
|
||||
net_type=NetType.MultipleClassifier,
|
||||
init_method=InitialMethod.Xavier,
|
||||
optimizer_name=OptimizerName.Momentum)
|
||||
|
||||
net = NeuralNet_4_2(params, "fashion_mnist_conv_test")
|
||||
|
||||
c1 = ConvLayer((1,28,28), (32,3,3), (1,0), params)
|
||||
net.add_layer(c1, "c1")
|
||||
r1 = ActivationLayer(Relu())
|
||||
net.add_layer(r1, "relu1")
|
||||
p1 = PoolingLayer(c1.output_shape, (2,2), 2, PoolingTypes.MAX)
|
||||
net.add_layer(p1, "p1")
|
||||
|
||||
f3 = FcLayer_2_0(p1.output_size, 128, params)
|
||||
net.add_layer(f3, "f3")
|
||||
r3 = ActivationLayer(Relu())
|
||||
net.add_layer(r3, "relu3")
|
||||
|
||||
f4 = FcLayer_2_0(f3.output_size, 10, params)
|
||||
net.add_layer(f4, "f4")
|
||||
s4 = ClassificationLayer(Softmax())
|
||||
net.add_layer(s4, "s4")
|
||||
|
||||
return net
|
||||
```
|
||||
|
||||
此模型只有一层卷积层,使用了32个卷积核,尺寸为3x3,后接最大池化层,然后两个全连接层。
|
||||
|
||||
#### 训练结果
|
||||
|
||||
训练10个epoch后得到如图18-39的曲线。
|
||||
|
||||
<img src="./img/18/FashionMnistLoss_cnn.png" />
|
||||
|
||||
图18-39 训练过程中损失函数值和准确度的变化
|
||||
|
||||
在测试集上得到91.12%的准确率,在测试集上的前几个样本的预测结果如图18-40所示。
|
||||
|
||||
<img src="./img/18/FashionMnistResult_cnn.png" ch="555" />
|
||||
|
||||
图18-40 测试结果
|
||||
|
||||
与前馈神经网络方案相比,这32个样本里只有一个错误,第4行最后一列,把第9类“短靴”预测成了“凉鞋”,因为这个样本中间有一个三角形的黑色块,与凉鞋的镂空设计很像。
|
||||
|
||||
### 代码位置
|
||||
|
||||
ch18, Level5
|
||||
|
|
|
@ -34,143 +34,11 @@ Cifar-10 由60000张32*32的 RGB 彩色图片构成,共10个分类。50000张
|
|||
|
||||
### 18.6.2 环境搭建
|
||||
|
||||
我们将使用Keras$^{[1]}$来训练模型,因为Keras是一个在TensorFlow平台上经过抽象的工具,它的抽象思想与我们在前面学习过的各种Layer的概念完全一致,有利于读者在前面的基础上轻松地继续学习。环境搭建有很多细节,我们在这里不详细介绍,只是把基本步骤列出。
|
||||
|
||||
1. 安装Python 3.6(本书中所有案例在Python 3.6上开发测试)
|
||||
2. 安装CUDA(没有GPU的读者请跳过)
|
||||
3. 安装cuDNN(没有GPU的读者请跳过)
|
||||
4. 安装TensorFlow,有GPU硬件的一定要按照GPU版,没有的只能安装CPU版
|
||||
5. 安装Keras
|
||||
|
||||
安装好后用pip list看一下,关键的几个包是:
|
||||
|
||||
```
|
||||
Package Version
|
||||
-------------------- ---------
|
||||
Keras 2.2.5
|
||||
Keras-Applications 1.0.8
|
||||
Keras-Preprocessing 1.1.0
|
||||
matplotlib 3.1.1
|
||||
numpy 1.17.0
|
||||
tensorboard 1.13.1
|
||||
tensorflow-estimator 1.13.0
|
||||
tensorflow-gpu 1.13.1
|
||||
```
|
||||
|
||||
如果没有GPU,则"tensorflow-gpu"一项会是"tensorflow"。
|
||||
|
||||
### 18.6.3 代码实现
|
||||
|
||||
```Python
|
||||
batch_size = 32
|
||||
num_classes = 10
|
||||
epochs = 25
|
||||
data_augmentation = True
|
||||
num_predictions = 20
|
||||
save_dir = os.path.join(os.getcwd(), 'saved_models')
|
||||
model_name = 'keras_cifar10_trained_model.h5'
|
||||
|
||||
# The data, split between train and test sets:
|
||||
(x_train, y_train), (x_test, y_test) = cifar10.load_data()
|
||||
print('x_train shape:', x_train.shape)
|
||||
print(x_train.shape[0], 'train samples')
|
||||
print(x_test.shape[0], 'test samples')
|
||||
|
||||
# Convert class vectors to binary class matrices.
|
||||
y_train = keras.utils.to_categorical(y_train, num_classes)
|
||||
y_test = keras.utils.to_categorical(y_test, num_classes)
|
||||
|
||||
model = Sequential()
|
||||
model.add(Conv2D(32, (3, 3), padding='same',
|
||||
input_shape=x_train.shape[1:]))
|
||||
model.add(Activation('relu'))
|
||||
model.add(Conv2D(32, (3, 3)))
|
||||
model.add(Activation('relu'))
|
||||
model.add(MaxPooling2D(pool_size=(2, 2)))
|
||||
model.add(Dropout(0.25))
|
||||
|
||||
model.add(Conv2D(64, (3, 3), padding='same'))
|
||||
model.add(Activation('relu'))
|
||||
model.add(Conv2D(64, (3, 3)))
|
||||
model.add(Activation('relu'))
|
||||
model.add(MaxPooling2D(pool_size=(2, 2)))
|
||||
model.add(Dropout(0.25))
|
||||
|
||||
model.add(Flatten())
|
||||
model.add(Dense(512))
|
||||
model.add(Activation('relu'))
|
||||
model.add(Dropout(0.5))
|
||||
model.add(Dense(num_classes))
|
||||
model.add(Activation('softmax'))
|
||||
|
||||
# initiate RMSprop optimizer
|
||||
opt = keras.optimizers.rmsprop(lr=0.0001, decay=1e-6)
|
||||
|
||||
# Let's train the model using RMSprop
|
||||
model.compile(loss='categorical_crossentropy',
|
||||
optimizer=opt,
|
||||
metrics=['accuracy'])
|
||||
|
||||
x_train = x_train.astype('float32')
|
||||
x_test = x_test.astype('float32')
|
||||
x_train /= 255
|
||||
x_test /= 255
|
||||
|
||||
if not data_augmentation:
|
||||
print('Not using data augmentation.')
|
||||
model.fit(x_train, y_train,
|
||||
batch_size=batch_size,
|
||||
epochs=epochs,
|
||||
validation_data=(x_test, y_test),
|
||||
shuffle=True)
|
||||
else:
|
||||
...
|
||||
```
|
||||
|
||||
在这个模型中:
|
||||
|
||||
1. 先用卷积->激活->卷积->激活->池化->丢弃层,做为第一梯队,卷积核32个;
|
||||
2. 然后再用卷积->激活->卷积->激活->池化->丢弃层做为第二梯队,卷积核64个;
|
||||
3. Flatten和Dense相当于把池化的结果转成Nx512的全连接层,N是池化输出的尺寸,被Flatten扁平化了;
|
||||
4. 再接丢弃层,避免过拟合;
|
||||
5. 最后接10个神经元的全连接层加Softmax输出。
|
||||
|
||||
为什么每一个梯队都要接一个DropOut层呢?因为这个网络结果设计已经比较复杂了,对于这个问题来说很可能会过拟合,所以要避免过拟合。如果简化网络结构,又可能会造成训练时间过长而不收敛。
|
||||
|
||||
### 18.6.4 训练结果
|
||||
|
||||
#### 在GPU上训练
|
||||
|
||||
在GPU上训练,每一个epoch大约需要1分钟;而在一个8核的CPU上训练,每个epoch大约需要2分钟(据笔者观察是因为并行计算占满了8个核)。所以即使读者没有GPU,用CPU训练还是可以接受的。以下是在GPU上的训练输出:
|
||||
|
||||
```
|
||||
Epoch 1/25
|
||||
1563/1563 [==============================] - 33s 21ms/step - loss: 1.8770 - acc: 0.3103 - val_loss: 1.6447 - val_acc: 0.4098
|
||||
......
|
||||
Epoch 25/25
|
||||
1563/1563 [==============================] - 87s 55ms/step - loss: 0.8809 - acc: 0.6960 - val_loss: 0.7724 - val_acc: 0.7372
|
||||
|
||||
Test loss: 0.772429921245575
|
||||
Test accuracy: 0.7372
|
||||
```
|
||||
经过25轮后,模型在测试集上的准确度为73.72%。
|
||||
|
||||
#### 在CPU上训练
|
||||
|
||||
在CPU上训练,只设置了10个epoch,一共半个小时时间,在测试集上达到63.61%的准确率。观察val_loss和val_acc的趋势,随着训练次数的增加,还可以继续优化。
|
||||
|
||||
```
|
||||
Epoch 1/10
|
||||
1563/1563 [==============================] - 133s 85ms/step - loss: 1.8563 - acc: 0.3198 - val_loss: 1.5658 - val_acc: 0.4343
|
||||
......
|
||||
|
||||
Epoch 10/10
|
||||
1563/1563 [==============================] - 131s 84ms/step - loss: 1.0972 - acc: 0.6117 - val_loss: 1.0426 - val_acc: 0.6361
|
||||
|
||||
10000/10000 [==============================] - 7s 684us/step
|
||||
Test loss: 1.042622245979309
|
||||
Test accuracy: 0.6361
|
||||
```
|
||||
|
||||
### 代码位置
|
||||
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
|
||||
## 19.1 两个时间步的循环神经网络
|
||||
|
||||
本小节中,我们将学习具有两个时间步的前馈神经网络组成的简单循环神经网络,用于实现回归/拟合功能。
|
||||
以下为本小节目录,详情请参阅《智能之门》正版图书,高等教育出版社。
|
||||
|
||||
### 19.1.1 提出问题
|
||||
|
||||
|
@ -45,286 +45,14 @@ def echo(x1,x2):
|
|||
|
||||
### 19.1.2 准备数据
|
||||
|
||||
与前面前馈神经网络和卷积神经网络中使用的样本数据的形状不同,在循环神经网络中的样本数据为三维:
|
||||
|
||||
- 第一维:样本 x[0,:,:]表示第0个样本
|
||||
- 第二维:时间 x[:,1,:]表示第1个时间点
|
||||
- 第三维:特征 x[:,:,2]表示第2个特征
|
||||
|
||||
举个例子来说,x[10, 5, 4] 表示第10个样本的第5个时间点的第4个特征值数据。
|
||||
|
||||
标签数据为两维:
|
||||
|
||||
- 第一维:样本
|
||||
- 第二维:标签值
|
||||
|
||||
所以,在本问题中,样本数据如表19-2所示。
|
||||
|
||||
表19-2 样本数据形状
|
||||
|
||||
|样本|特征值|标签值|
|
||||
|---|---|---|
|
||||
|0|0.35|0|
|
||||
|1|0.46|0.35|
|
||||
|2|0.12|0.46|
|
||||
|3|0.69|0.12|
|
||||
|4|0.24|0.69|
|
||||
|5|0.94|0.24|
|
||||
|...|...|...|
|
||||
|
||||
|
||||
### 19.1.3 用前馈神经网络的知识来解决问题
|
||||
|
||||
#### 搭建网络
|
||||
|
||||
我们回忆一下,在验证万能近似定理时,我们学习了曲线拟合问题,即带有一个隐层和非线性激活函数的前馈神经网络,可以拟合任意曲线。但是在这个问题里,有几点不同:
|
||||
|
||||
1. 不是连续值,而是时间序列的离散值
|
||||
2. 完全随机的离散值,而不是满足一定的规律
|
||||
3. 测试数据不在样本序列里,完全独立
|
||||
|
||||
所以,即使使用前馈神经网络中的曲线拟合技术得到了一个拟合网络,也不能正确地预测不在样本序列里的测试集数据。但是,我们可以把前馈神经网络做一个变形,让它能够处理时间序列数据,如图19-8所示。
|
||||
|
||||
<img src="./img/19/random_number_echo_net.png"/>
|
||||
|
||||
图19-8 两个时间步的前馈神经网络
|
||||
|
||||
图19-8中含有两个简单的前馈神经网络,t1和t2,每个节点上都只有一个神经元,其中,各个节点的名称和含义如表19-3所示。
|
||||
|
||||
表19-3 图19-8中的各个节点的名称和含义
|
||||
|
||||
|名称|含义|在t1,t2上的取值|
|
||||
|---|---|---|
|
||||
|x|输入层样本|根据样本值|
|
||||
|U|x到h的权重值|相同|
|
||||
|h|隐层|依赖于x的值|
|
||||
|bh|h节点的偏移值|相同|
|
||||
|tanh|激活函数|函数形式相同|
|
||||
|s|隐层激活状态|依赖于h的值|
|
||||
|V|s到z的权重值|相同|
|
||||
|z|输出层|依赖于s的值|
|
||||
|bz|z的偏移值|相同|
|
||||
|loss|损失函数|函数形式相同|
|
||||
|y|标签值|根据标签值|
|
||||
|
||||
由于是一个拟合值的网络,相当于线性/非线性回归,所以在输出层不使用分类函数,损失函数使用均方差。在这个具体的问题中,t2的标签值y应该和t1的样本值x相同。
|
||||
|
||||
请读者注意,在很多关于循环神经网络的文字资料中,通常把h和s合并在一起。在这里我们把它们分开画,便于后面的反向传播的推导和理解。
|
||||
|
||||
还有一个问题是,为什么t1的后半部分是虚线的?因为在这个问题中,我们只对t2的输出感兴趣,检测t2的输出值z和y的差距是多少,而不关心t1的输出是什么,所以不必计算t1的z值和损失函数值,处于无监督状态。
|
||||
|
||||
#### 前向计算
|
||||
|
||||
t1和t2是两个独立的网络,在t1和t2之间,用一个W连接t1的隐层激活状态值到t2的隐层输入,对t2来说,相当于有两个输入:一个是t2时刻的样本值x,一个是t1时刻的隐层激活值s。所以,它们的前向计算公式为:
|
||||
|
||||
对于t1:
|
||||
|
||||
$$
|
||||
h_{t1}=x_{t1} \cdot U + b_h\tag{1}
|
||||
$$
|
||||
|
||||
$$
|
||||
s_{t1} = Tanh(h_{t1}) \tag{2}
|
||||
$$
|
||||
|
||||
对于t2:
|
||||
$$
|
||||
h_{t2}=x_{t2} \cdot U + s_{t1} \cdot W +b_h\tag{3}
|
||||
$$
|
||||
|
||||
$$
|
||||
s_{t2} = Tanh(h_{t2}) \tag{4}
|
||||
$$
|
||||
|
||||
$$
|
||||
z_{t2} = s_{t2} \cdot V + b_z\tag{5}
|
||||
$$
|
||||
|
||||
$$
|
||||
loss = \frac{1}{2}(z_{t2}-y_{t2})^2\tag{6}
|
||||
$$
|
||||
|
||||
在本例中,公式1至公式6中,所有的变量均为标量,这就有利于我们对反向传播的推导,不用考虑矩阵、向量的求导运算。
|
||||
|
||||
本来整体的损失函数值$loss$应该是两个时间步的损失函数值之和,但是第一个时间步没有输出,所以不需要计算损失函数值,因此$loss$就等于第二个时间步的损失函数值。
|
||||
|
||||
#### 反向传播
|
||||
|
||||
我们首先对t2网络进行反向传播推导:
|
||||
|
||||
$$
|
||||
\frac{\partial loss}{\partial z_{t2}}=z_{t2}-y_{t2} \rightarrow dz_{t2} \tag{7}
|
||||
$$
|
||||
$$
|
||||
\begin{aligned}
|
||||
\frac{\partial loss}{\partial h_{t2}}&=\frac{\partial loss}{\partial z_{t2}}\frac{\partial z_{t2}}{\partial s_{t2}}\frac{\partial s_{t2}}{\partial h_{t2}} \\\\
|
||||
&=dz_{t2} \cdot V^{\top} \odot Tanh'(s_{t2}) \\\\
|
||||
&=dz_{t2} \cdot V^{\top} \odot (1-s_{t2}^2) \rightarrow dh_{t2}
|
||||
\end{aligned} \tag{8}
|
||||
$$
|
||||
|
||||
$$
|
||||
\frac{\partial loss}{\partial b_z}=\frac{\partial loss}{\partial z_{t2}}\frac{\partial z_{t2}}{\partial b_z}=dz_{t2} \rightarrow db_{z_{t2}} \tag{9}
|
||||
$$
|
||||
|
||||
$$
|
||||
\frac{\partial loss}{\partial b_h}=\frac{\partial loss}{\partial h_{t2}}\frac{\partial h_{t2}}{\partial b_h}=dh_{t2} \rightarrow db_{h_{t2}} \tag{10}
|
||||
$$
|
||||
|
||||
$$
|
||||
\frac{\partial loss}{\partial V}=\frac{\partial loss}{\partial z_{t2}}\frac{\partial z_{t2}}{\partial V}=s_{t2}^{\top} \cdot dz_{t2} \rightarrow dV_{t2} \tag{11}
|
||||
$$
|
||||
|
||||
$$
|
||||
\frac{\partial loss}{\partial U}=\frac{\partial loss}{\partial h_{t2}}\frac{\partial h_{t2}}{\partial U}=x_{t2}^{\top} \cdot dh_{t2} \rightarrow dU_{t2} \tag{12}
|
||||
$$
|
||||
|
||||
$$
|
||||
\frac{\partial loss}{\partial W}=\frac{\partial loss}{\partial h_{t2}}\frac{\partial h_{t2}}{\partial W}=s_{t1}^{\top} \cdot dh_{t2} \rightarrow dW_{t2} \tag{13}
|
||||
$$
|
||||
|
||||
下面我们对t1网络进行反向传播推导。由于t1是没有输出的,所以我们不必考虑后半部分的反向传播问题,只从s节点开始向后计算。
|
||||
|
||||
$$
|
||||
\frac{\partial loss}{\partial h_{t1}}=\frac{\partial loss}{\partial h_{t2}}\frac{\partial h_{t2}}{\partial s_{t1}}\frac{\partial s_{t1}}{\partial h_{t1}}=dh_{t2} \cdot W^{\top} \odot (1-s_{t1}^2) \rightarrow dh_{t1} \tag{14}
|
||||
$$
|
||||
|
||||
$$
|
||||
\frac{\partial loss}{\partial b_h}=\frac{\partial loss}{\partial h_{t1}}\frac{\partial h_{t1}}{\partial b_h}=dh_{t1} \rightarrow db_{h_{t1}} \tag{15}
|
||||
$$
|
||||
$$
|
||||
db_{z_{t1}} = 0 \tag{16}
|
||||
$$
|
||||
$$
|
||||
\frac{\partial loss}{\partial U}=\frac{\partial loss}{\partial h_{t1}}\frac{\partial h_{t1}}{\partial U}=x_{t1}^{\top} \cdot dh_{t1} \rightarrow dU_{t1} \tag{17}
|
||||
$$
|
||||
$$
|
||||
dV_{t1} = 0 \tag{18}
|
||||
$$
|
||||
$$
|
||||
dW_{t1}=0 \tag{19}
|
||||
$$
|
||||
|
||||
#### 梯度更新
|
||||
|
||||
到目前为止,我们得到了两个时间步内部的所有参数的误差值,如何更新参数呢?因为在循环神经网络中,$U、V、W、bz、bh$都是共享的,所以不能单独更新独立时间步中的参数,而是要一起更新。
|
||||
|
||||
$$
|
||||
U = U - \eta \cdot (dU_{t1} + dU_{t2})
|
||||
$$
|
||||
|
||||
$$
|
||||
V = V - \eta \cdot (dV_{t1} + dV_{t2})
|
||||
$$
|
||||
|
||||
$$
|
||||
W = W - \eta \cdot (dW_{t1} + dW_{t2})
|
||||
$$
|
||||
|
||||
$$
|
||||
b_h = b_h - \eta \cdot (db_{ht1} + db_{ht2})
|
||||
$$
|
||||
|
||||
$$
|
||||
b_z = b_z - \eta \cdot (db_{zt1} + db_{zt2})
|
||||
$$
|
||||
|
||||
|
||||
### 19.1.4 代码实现
|
||||
|
||||
按照图19-8的设计,我们实现两个前馈神经网络来模拟两个时序。
|
||||
|
||||
#### 时序1的网络实现
|
||||
|
||||
时序1的类名叫做timestep_1,其前向计算过程遵循公式1、2,其反向传播过程遵循公式14至公式19。
|
||||
|
||||
```Python
|
||||
class timestep_1(object):
|
||||
def forward(self,x,U,V,W,bh):
|
||||
...
|
||||
|
||||
def backward(self, y, dh_t2):
|
||||
...
|
||||
```
|
||||
|
||||
#### 时序2的网络实现
|
||||
|
||||
时序2的类名叫做timestep_2,其前向计算过程遵循公式3至公式5,其反向传播过程遵循公式7至公式13。
|
||||
|
||||
```Python
|
||||
class timestep_2(object):
|
||||
def forward(self,x,U,V,W,bh,bz,s_t1):
|
||||
...
|
||||
|
||||
def backward(self, y, s_t1):
|
||||
...
|
||||
```
|
||||
|
||||
#### 网络训练代码
|
||||
|
||||
在初始化函数中,先建立好一些基本的类,如损失函数计算、训练历史记录,再建立好两个时序的类,分别命名为t1和t2。
|
||||
|
||||
```Python
|
||||
class net(object):
|
||||
def __init__(self, dr):
|
||||
self.dr = dr
|
||||
self.loss_fun = LossFunction_1_1(NetType.Fitting)
|
||||
self.loss_trace = TrainingHistory_3_0()
|
||||
self.t1 = timestep_1()
|
||||
self.t2 = timestep_2()
|
||||
```
|
||||
|
||||
在训练函数中,仍然采用DNN/CNN中学习过的双重循环的方法,外循环为epoch,内循环为iteration,每次只用一个样本做训练,分别取出它的时序1和时序2的样本值和标签值,先做前向计算,再做反向传播,然后更新参数。
|
||||
|
||||
```Python
|
||||
def train(self):
|
||||
...
|
||||
for epoch in range(max_epoch):
|
||||
for iteration in range(max_iteration):
|
||||
...
|
||||
...
|
||||
```
|
||||
|
||||
### 19.1.5 运行结果
|
||||
|
||||
<img src="./img/19/random_number_echo_loss.png">
|
||||
|
||||
图19-9 损失函数值和准确度的历史记录曲线
|
||||
|
||||
从图19-9的训练过程看,网络收敛情况比较理想。由于使用单样本训练,所以训练集的损失函数变化曲线和准确度变化曲线计算不准确,所以在图中没有画出,下同。
|
||||
|
||||
以下是打印输出的最后几行信息:
|
||||
|
||||
```
|
||||
...
|
||||
98
|
||||
loss=0.001396, acc=0.952491
|
||||
99
|
||||
loss=0.001392, acc=0.952647
|
||||
testing...
|
||||
loss=0.002230, acc=0.952609
|
||||
```
|
||||
|
||||
使用完全不同的测试集数据,得到的准确度为95.26%。最后在测试集上得到的拟合结果如图19-10所示。
|
||||
|
||||
<img src="./img/19/random_number_echo_result.png"/>
|
||||
|
||||
图19-10 测试集上的拟合结果
|
||||
|
||||
红色x是测试集样本,蓝色圆点是模型的预测值,可以看到波动的趋势全都预测准确,具体的值上面有一些微小的误差。
|
||||
|
||||
以下是训练出来的各个参数的值:
|
||||
|
||||
```
|
||||
U=[[-0.54717934]], bh=[[0.26514691]],
|
||||
V=[[0.50609376]], bz=[[0.53271514]],
|
||||
W=[[-4.39099762]]
|
||||
```
|
||||
|
||||
可以看到W的值比其他值大出一个数量级,对照图19-8理解,这就意味着在t2上的输出主要来自于t1的样本输入,这也符合我们的预期,即:接收到两个序列的数值时,返回第一个序列的数值。
|
||||
|
||||
至此,我们解决了本章开始时提出的问题。注意,我们没有使用到循环神经网络的任何概念,而是完全通过以前学习到的前馈神经网络的概念来做正向和反向推导。但是通过t1、t2两个时序的衔接,我们已经可以体会到循环神经网络的妙处了,后面我们会用它来解决更复杂的问题。
|
||||
|
||||
### 代码位置
|
||||
|
||||
ch19, Level1
|
||||
|
|
|
@ -3,7 +3,8 @@
|
|||
|
||||
## 19.2 四个时间步的循环神经网络
|
||||
|
||||
本小节中,我们将学习具有四个时间步的循环神经网络,用于二分类功能。
|
||||
以下为本小节目录,详情请参阅《智能之门》正版图书,高等教育出版社。
|
||||
|
||||
|
||||
### 19.2.1 提出问题
|
||||
|
||||
|
@ -34,387 +35,18 @@
|
|||
|
||||
### 19.2.2 准备数据
|
||||
|
||||
由于计算是从最后一位开始的,我们认为最后一位是第一个时间步,所以需要把样本数据的前后顺序颠倒一下,比如13,从二进制的 [1, 1, 0, 1] 倒序变成 [1, 0, 1, 1]。相应地,标签数据7也要从二进制的 [0, 1, 1, 1] 倒序变成 [1, 1, 1, 0]。
|
||||
|
||||
在这个例子中,因为是4位二进制减法,所以最大值是15,即 [1, 1, 1, 1];最小值是0,并且要求被减数必须大于减数,所以样本的数量一共是136个,每个样本含有两组4位的二进制数,表示被减数和减数。标签值为一组4位二进制数。三组二进制数都是倒序。
|
||||
|
||||
所以,仍以13-6=7为例,单个样本如表19-4所示。
|
||||
|
||||
表19-4 以13-6=7为例的单个样本
|
||||
|
||||
|时间步|特征值1|特征值2|标签值|
|
||||
|---|---|---|---|
|
||||
|1(最低位)|1|0|1|
|
||||
|2|0|1|1|
|
||||
|3|1|1|1|
|
||||
|4(最高位)|1|0|0|
|
||||
|
||||
为了和图19-11保持一致,我们令时间步从1开始(但编程时是从0开始的)。特征值1从下向上看是[1101],即十进制13;特征值2从下向上看是[0110],即十进制6;标签值从下向上看是[0111],即十进制6。
|
||||
|
||||
所以,单个样本是一个二维数组,而多个样本就是三维数组,第一维是样本,第二维是时间步,第三维是特征值。
|
||||
|
||||
### 19.2.3 搭建多个时序的网络
|
||||
|
||||
#### 搭建网络
|
||||
|
||||
在本例中,我们仍然从前馈神经网络的结构扩展到含有4个时序的循环神经网络结构,如图19-11所示。
|
||||
|
||||
<img src="./img/19/binary_number_minus_net.png"/>
|
||||
|
||||
图19-11 含有4个时序的网络结构图
|
||||
|
||||
图19-11中,最左侧的简易结构是通常的循环神经网络的画法,而右侧是其展开后的细节,由此可见细节有很多,如果不展开的话,对于初学者来说很难理解,而且也不利于我们进行反向传播的推导。
|
||||
|
||||
与19.1节不同的是,在每个时间步的结构中,多出来一个a,是从z经过二分类函数生成的。这是为什么呢?因为在本例中,我们想模拟二进制数的减法,所以结果应该是0或1,于是我们把它看作是二分类问题,z的值是一个浮点数,用二分类函数后,使得a的值尽量向两端(0或1)靠近,但是并不能真正地达到0或1,只要大于0.5就认为是1,否则就认为是0。
|
||||
|
||||
二分类问题的损失函数使用交叉熵函数,这与我们在前面学习的二分类问题完全相同。
|
||||
|
||||
再重复一下,请读者记住,t1是二进制数的最低位,但是由于我们把样本倒序了,所以,现在的t1就是样本的第0个单元的值。并且由于涉及到被减数和减数,所以每个样本的第0个单元(时间步)都有两个特征值,其它3个单元也一样。
|
||||
|
||||
在19.1节的例子中,连接x和h的是一条线标记为U,U是一个标量参数;在图19-11中,由于隐层神经元数量为4,所以U是一个 1x4 的参数矩阵,V是一个 4x1 的参数矩阵,而W就是一个 4x4 的参数矩阵。我们把它们展开画成图19-12(其中把s和h合并在一起了)。
|
||||
|
||||
<img src="./img/19/binary_number_minus_unfold.png"/>
|
||||
|
||||
图19-12 W权重矩阵的展开图
|
||||
|
||||
U和V都比较容易理解,而W是一个连接相邻时序的参数矩阵,并且共享相同的参数值,这一点在刚开始接触循环神经网络时不太容易理解。图19-12中把W绘制成3种颜色,代表它们在不同的时间步中的作用,是想让读者看得清楚些,并不代表它们是不同的值。
|
||||
|
||||
### 19.2.4 正向计算
|
||||
|
||||
下面我们先看看4个时序的正向计算过程。
|
||||
|
||||
从图一中看,t2、t3、t4的结构是一样的,只有t1缺少了从前面的时间步的输入,因为它是第一个时序,前面没有输入,所以我们单独定义t1的前向计算函数:
|
||||
|
||||
$$
|
||||
h = x \cdot U \tag{1}
|
||||
$$
|
||||
|
||||
$$
|
||||
s = Tanh(h) \tag{2}
|
||||
$$
|
||||
|
||||
$$
|
||||
z = s \cdot V \tag{3}
|
||||
$$
|
||||
|
||||
$$
|
||||
a = Logistic(z) \tag{4}
|
||||
$$
|
||||
|
||||
在公式1和公式3中,我们并没有添加偏移项b,是因为在此问题中,没有偏移项一样可以完成任务。
|
||||
|
||||
单个时间步的损失函数值:
|
||||
|
||||
$$
|
||||
loss_t = -[y_t \ln a_t + (1-y_t) \ln (1-a_t)]
|
||||
$$
|
||||
|
||||
所有时间步的损失函数值计算:
|
||||
|
||||
$$
|
||||
Loss = \frac{1}{4} \sum_{t=1}^4 loss_t \tag{5}
|
||||
$$
|
||||
|
||||
公式5中的$Loss$表示每个时间步的$loss_t$之和。
|
||||
|
||||
```Python
|
||||
class timestep_1(timestep):
|
||||
def forward(self,x,U,V,W):
|
||||
self.U = U
|
||||
self.V = V
|
||||
self.W = W
|
||||
self.x = x
|
||||
# 公式1
|
||||
self.h = np.dot(self.x, U)
|
||||
# 公式2
|
||||
self.s = Tanh().forward(self.h)
|
||||
# 公式3
|
||||
self.z = np.dot(self.s, V)
|
||||
# 公式4
|
||||
self.a = Logistic().forward(self.z)
|
||||
```
|
||||
|
||||
其它三个时间步的前向计算过程是一样的,它们与t1的不同之处在于公式1,所以我们单独说明一下:
|
||||
|
||||
$$
|
||||
h = x \cdot U + s_{t-1} \cdot W \tag{6}
|
||||
$$
|
||||
|
||||
```Python
|
||||
class timestep(object):
|
||||
def forward(self,x,U,V,W,prev_s):
|
||||
...
|
||||
# 公式6
|
||||
self.h = np.dot(x, U) + np.dot(prev_s, W)
|
||||
...
|
||||
```
|
||||
|
||||
### 19.2.5 反向传播
|
||||
|
||||
反向传播的计算对于4个时间步来说,分为3种过程,但是它们之间只有微小的区别。我们先把公共的部分列出来,再说明每个时间步的差异。
|
||||
|
||||
首先是损失函数对z节点的偏导数,对于4个时间步来说都一样:
|
||||
|
||||
$$
|
||||
\begin{aligned}
|
||||
\frac{\partial loss_t}{\partial z_t}&=\frac{\partial loss_t}{\partial a_t}\frac{\partial a_t}{\partial z_t} \\\\
|
||||
&= a_t - y_t \rightarrow dz_t
|
||||
\end{aligned}
|
||||
\tag{7}
|
||||
$$
|
||||
|
||||
再进一步计算s和h的误差。对于t4来说,s和h节点的路径比较单一,直接从z节点向下反向推导即可:
|
||||
|
||||
$$
|
||||
\frac{\partial loss_{t4}}{\partial s_{t4}}=\frac{\partial loss_{t4}}{\partial z_{t4}}\frac{\partial z_{t4}}{\partial s_{t4}} = dz_{t4} \cdot V^{\top} \tag{8}
|
||||
$$
|
||||
|
||||
$$
|
||||
\frac{\partial loss_{t4}}{\partial h_{t4}}=\frac{\partial loss_{t4}}{\partial s_{t4}}\frac{\partial s_{t4}}{\partial h_{t4}}=dz_{t4} \cdot V^{\top} \odot Tanh'(s_{t4}) \rightarrow dh_{t4} \tag{9}
|
||||
$$
|
||||
|
||||
提醒两点:
|
||||
|
||||
1. 公式8、9中用了$loss_{t4}$而不是$Loss$,因为只针对第4个时间步,而不是所有时间步。
|
||||
2. 出现了$V^{\top}$,因为在本例中V是一个矩阵,而非标量,在求导时需要转置。
|
||||
|
||||
对于t1、t2、t3的s节点来说,都有两个方向的反向路径,第一个是从本时间步的z节点,第二个是从后一个时间步的h节点,因此,s的反向计算应该是两个路径的和。
|
||||
|
||||
我们先以t3为例推导:
|
||||
|
||||
$$
|
||||
\begin{aligned}
|
||||
\frac{\partial Loss}{\partial s_{t3}}&=\frac{\partial loss_{t3}}{\partial s_{t3}} + \frac{\partial loss_{t4}}{\partial s_{t3}}
|
||||
\\\\
|
||||
&=\frac{\partial loss_{t3}}{\partial s_{t3}} + \frac{\partial loss_{t4}}{\partial h_{t4}}\frac{\partial h_{t4}}{\partial s_{t3}} \\\\
|
||||
&=dz_{t3} \cdot V^{\top} + dh_{t4} \cdot W^{\top}
|
||||
\end{aligned}
|
||||
$$
|
||||
|
||||
再扩展到一般情况:
|
||||
|
||||
$$
|
||||
\begin{aligned}
|
||||
\frac{\partial Loss}{\partial s_t}&=\frac{\partial loss_t}{\partial s_t} + \frac{\partial Loss}{\partial h_{t+1}}\frac{\partial h_{t+1}}{\partial s_t} \\\\
|
||||
&=dz_t \cdot V^{\top} + dh_{t+1} \cdot W^{\top}
|
||||
\end{aligned}
|
||||
\tag{10}
|
||||
$$
|
||||
|
||||
再进一步计算t1、t2、t3的h节点的误差:
|
||||
|
||||
$$
|
||||
\begin{aligned}
|
||||
\frac{\partial Loss}{\partial h_t} &= \frac{\partial Loss}{\partial s_t} \frac{\partial s_t}{\partial h_t} \\\\
|
||||
&= (dz \cdot V^{\top} + dh_{t+1} \cdot W^{\top} ) \odot Tanh'(s_t) \rightarrow dh_t
|
||||
\end{aligned}
|
||||
\tag{11}
|
||||
$$
|
||||
|
||||
下面计算V的误差,V只与z节点和s节点有关,而且4个时间步是相同的:
|
||||
|
||||
$$
|
||||
\frac{\partial loss_t}{\partial V_t}=\frac{\partial loss_t}{\partial z_t}\frac{\partial z_t}{\partial V_t}=s_t^{\top} \cdot dz_t \rightarrow dV_t \tag{12}
|
||||
$$
|
||||
|
||||
下面计算U的误差,U只与节点h和输入x有关,而且4个时间步是相同的,但是U参与了所有时间步的计算,因此要用 $Loss$ 求 $U_t$ 的偏导:
|
||||
|
||||
$$
|
||||
\frac{\partial Loss}{\partial U_t}=\frac{\partial Loss}{\partial h_t}\frac{\partial h_t}{\partial U_t}=x_t^{\top} \cdot dh_t \rightarrow dU_t \tag{13}
|
||||
$$
|
||||
|
||||
下面计算W的误差,从图19-11中看,t1没有W参与计算的,与其它三个时间步不同,所以对于t1来说:
|
||||
|
||||
$$
|
||||
dW_{t1} = 0 \tag{14}
|
||||
$$
|
||||
|
||||
对于t2、t3、t4:
|
||||
|
||||
$$
|
||||
\frac{\partial Loss}{\partial W_t}=\frac{\partial Loss}{\partial h_t}\frac{\partial h_t}{\partial W_t}=s_{t-1}^{\top} \cdot dh_{t} \rightarrow dW_{t} \tag{15}
|
||||
$$
|
||||
|
||||
下面是t1的反向传播函数,与其他3个t不同的是dW部分为0:
|
||||
|
||||
```Python
|
||||
class timestep_1(timestep):
|
||||
def backward(self, y, next_dh):
|
||||
...
|
||||
self.dh = (np.dot(self.dz, self.V.T) + np.dot(next_dh, self.W.T)) * Tanh().backward(self.s)
|
||||
self.dW = 0
|
||||
```
|
||||
|
||||
下面是t2、t3的反向传播函数:
|
||||
|
||||
```Python
|
||||
class timestep(object):
|
||||
def backward(self, y, prev_s, next_dh):
|
||||
...
|
||||
self.dh = (np.dot(self.dz, self.V.T) + np.dot(next_dh, self.W.T)) * Tanh().backward(self.s)
|
||||
self.dW = np.dot(prev_s.T, self.dh)
|
||||
```
|
||||
|
||||
下面是t4的反向传播函数,与前三个t不同的是dh的求导公式中少一项:
|
||||
|
||||
```Python
|
||||
class timestep_4(timestep):
|
||||
# compare with timestep class: no next_dh from future layer
|
||||
def backward(self, y, prev_s):
|
||||
...
|
||||
self.dh = np.dot(self.dz, self.V.T) * Tanh().backward(self.s)
|
||||
self.dW = np.dot(prev_s.T, self.dh)
|
||||
```
|
||||
|
||||
### 19.2.6 梯度更新
|
||||
|
||||
到目前为止,我们已经得到了所有时间步的关于所有参数的梯度,梯度更新时,由于参数共享,所以与19.1节中的方法一样,先要把所有时间步的相同参数的梯度相加,统一乘以学习率,与上一次的参数相减。
|
||||
|
||||
用一个通用的公式描述:
|
||||
|
||||
$$
|
||||
W_{next} = W_{current} - \eta \cdot \sum_{t=1}^{\tau} dW_t
|
||||
$$
|
||||
|
||||
其中,$W$可以换成 $U、V$ 等参数。
|
||||
|
||||
### 19.2.7 代码实现
|
||||
|
||||
在上一小节我们已经讲解了正向和反向的代码实现,本小节讲一下训练部分的主要代码。
|
||||
|
||||
#### 初始化
|
||||
|
||||
初始化 loss function 和 loss trace,然后初始化4个时间步的实例。注意t2和t3使用了相同的类timestep。
|
||||
|
||||
```Python
|
||||
class net(object):
|
||||
def __init__(self, dr):
|
||||
...
|
||||
self.t1 = timestep_1()
|
||||
self.t2 = timestep()
|
||||
self.t3 = timestep()
|
||||
self.t4 = timestep_4()
|
||||
```
|
||||
|
||||
#### 前向计算
|
||||
|
||||
按顺序分别调用4个时间步的前向计算函数,注意在t2到t4时,需要把t-1时刻的s值代进去。
|
||||
|
||||
```Python
|
||||
def forward(self,X):
|
||||
self.t1.forward(X[:,0],self.U,self.V,self.W)
|
||||
self.t2.forward(X[:,1],self.U,self.V,self.W,self.t1.s)
|
||||
self.t3.forward(X[:,2],self.U,self.V,self.W,self.t2.s)
|
||||
self.t4.forward(X[:,3],self.U,self.V,self.W,self.t3.s)
|
||||
```
|
||||
|
||||
#### 反向传播
|
||||
|
||||
按相反的顺序调用4个时间步的反向传播函数,注意在t3、t2、t1时,要把t+1时刻的dh代进去,以便计算当前时刻的dh;而在t4、t3、t2时,需要把t-1时刻的s值代进去,以便计算dW的值。
|
||||
|
||||
```Python
|
||||
def backward(self,Y):
|
||||
self.t4.backward(Y[:,3], self.t3.s)
|
||||
self.t3.backward(Y[:,2], self.t2.s, self.t4.dh)
|
||||
self.t2.backward(Y[:,1], self.t1.s, self.t3.dh)
|
||||
self.t1.backward(Y[:,0], self.t2.dh)
|
||||
```
|
||||
|
||||
#### 更新参数
|
||||
|
||||
在参数更新部分,需要把4个时间步的参数梯度相加再乘以学习率,做为整个网络的梯度。
|
||||
|
||||
```Python
|
||||
def update(self, eta):
|
||||
self.U = self.U - (self.t1.dU + self.t2.dU + self.t3.dU + self.t4.dU)*eta
|
||||
self.V = self.V - (self.t1.dV + self.t2.dV + self.t3.dV + self.t4.dV)*eta
|
||||
self.W = self.W - (self.t1.dW + self.t2.dW + self.t3.dW + self.t4.dW)*eta
|
||||
```
|
||||
|
||||
#### 损失函数
|
||||
|
||||
4个时间步都参与损失函数计算,所以总体的损失函数是4个时间步的损失函数值的和。
|
||||
|
||||
```Python
|
||||
def check_loss(self,X,Y):
|
||||
......
|
||||
Loss = (loss1 + loss2 + loss3 + loss4)/4
|
||||
return Loss,acc,result
|
||||
```
|
||||
|
||||
#### 训练过程
|
||||
|
||||
用双重循环进行训练,每次只用一个样本,因此batch_size=1。
|
||||
|
||||
```Python
|
||||
def train(self, batch_size, checkpoint=0.1):
|
||||
...
|
||||
for epoch in range(max_epoch):
|
||||
dr.Shuffle()
|
||||
for iteration in range(max_iteration):
|
||||
# get data
|
||||
batch_x, batch_y = self.dr.GetBatchTrainSamples(1, iteration)
|
||||
# forward
|
||||
self.forward(batch_x)
|
||||
# backward
|
||||
self.backward(batch_y)
|
||||
# update
|
||||
self.update(eta)
|
||||
# check loss
|
||||
...
|
||||
```
|
||||
|
||||
### 19.2.8 运行结果
|
||||
|
||||
我们设定在验证集上的准确率为1.0时即停止训练,图19-13为训练过程曲线。
|
||||
|
||||
<img src="./img/19/binary_number_minus_loss.png"/>
|
||||
|
||||
图19-13 训练过程中的损失函数和准确率变化
|
||||
|
||||
下面是最后几轮的打印输出结果:
|
||||
|
||||
```
|
||||
...
|
||||
5 741 loss=0.156525, acc=0.867647
|
||||
5 755 loss=0.131925, acc=0.963235
|
||||
5 811 loss=0.106093, acc=1.000000
|
||||
testing...
|
||||
loss=0.105319, acc=1.000000
|
||||
```
|
||||
我们在验证集上(实际上和测试集一致)得到了100%的准确率,即所有136个测试样本都可以得到正确的预测值。
|
||||
|
||||
下面随机列出了几个测试样本及其预测结果:
|
||||
|
||||
```
|
||||
x1: [1, 0, 1, 1]
|
||||
- x2: [0, 0, 0, 1]
|
||||
------------------
|
||||
true: [1, 0, 1, 0]
|
||||
pred: [1, 0, 1, 0]
|
||||
11 - 1 = 10
|
||||
====================
|
||||
|
||||
x1: [1, 1, 1, 1]
|
||||
- x2: [0, 0, 1, 1]
|
||||
------------------
|
||||
true: [1, 1, 0, 0]
|
||||
pred: [1, 1, 0, 0]
|
||||
15 - 3 = 12
|
||||
====================
|
||||
|
||||
x1: [1, 1, 0, 1]
|
||||
- x2: [0, 1, 1, 0]
|
||||
------------------
|
||||
true: [0, 1, 1, 1]
|
||||
pred: [0, 1, 1, 1]
|
||||
13 - 6 = 7
|
||||
====================
|
||||
```
|
||||
|
||||
我们如何理解循环神经网络的概念在这个问题中的作用呢?
|
||||
|
||||
在每个时间步中,U、V负责的是0、1相减可以得到正确的值,而W的作用是借位,在相邻的时间步之间传递借位信息,以便当t-1时刻的计算发生借位时,在t时刻也可以得到正确的结果。
|
||||
|
||||
### 代码位置
|
||||
|
||||
|
|
|
@ -3,6 +3,8 @@
|
|||
|
||||
## 19.3 通用的循环神经网络模型
|
||||
|
||||
以下为本小节目录,详情请参阅《智能之门》正版图书,高等教育出版社。
|
||||
|
||||
### 19.3.1 提出问题
|
||||
|
||||
在19.1节和19.2节的情况不同,都是预知时间步长度,然后以纯手工方式搭建循环神经网络。表19-5展示了前两节和后面的章节要实现的循环神经网络参数。
|
||||
|
@ -32,298 +34,13 @@
|
|||
|
||||
### 19.3.2 全输出网络通过时间反向传播
|
||||
|
||||
前两节的内容详细地描述了循环神经网络中的反向传播的方法,尽管如表19-5所示,二者有些许不同,但是还是可以从中总结出关于梯度计算和存储的一般性的规律,即通过时间的反向传播(BPTT,Back Propagation Through Time)。
|
||||
|
||||
<img src="./img/19/bptt.png"/>
|
||||
|
||||
图19-14 全输出网路通过时间的反向传播
|
||||
|
||||
如图19-14所示,我们仍以具有4个时间步的循环神经网络为例,推导通用的反向传播算法,然后再扩展到一般性。每一个时间步都有输出,所以称为全输出网路,主要是为了区别与后面的单输出形式。
|
||||
|
||||
图中蓝色箭头线表示前向计算的过程,线上的字符表示连接参数,用于矩阵相乘。红色的箭头线表示反向传播的过程,线上的数字表示计算梯度的顺序。
|
||||
|
||||
正向计算过程为:
|
||||
|
||||
$$
|
||||
h_t = x_t \cdot U + s_{t-1} \cdot W + b_u \tag{1}
|
||||
$$
|
||||
|
||||
只有在时间步t1时,公式1中的$s_{t-1}$为0。
|
||||
|
||||
后续过程为:
|
||||
|
||||
$$
|
||||
s_t = \sigma (h_t) \tag{2}
|
||||
$$
|
||||
|
||||
$$
|
||||
z_t = V \cdot s_t + b_v \tag{3}
|
||||
$$
|
||||
|
||||
$$
|
||||
a_t = C(z_t) \tag{4}
|
||||
$$
|
||||
|
||||
$$
|
||||
loss_t = L(a_t, y_t) \tag{5}
|
||||
$$
|
||||
|
||||
公式2中的$\sigma$是激活函数,一般为Tanh函数,但也不排除使用其它函数。公式4中的分类函数 $C$ 和公式5中的损失函数 $L$ 因不同的网络类型而不同,如表19-5所示。
|
||||
|
||||
$$
|
||||
Loss = \frac{1}{\tau} \sum_t^{\tau}loss_t \tag{6}
|
||||
$$
|
||||
|
||||
公式6中的$\tau$表示最大时间步数,或最后一个时间步数。
|
||||
|
||||
$$
|
||||
\frac{\partial loss_t}{\partial z_t} = a_t - y_t \rightarrow dz_t
|
||||
\tag{7}
|
||||
$$
|
||||
|
||||
对于图19-14的最后一个时间步来说,节点s和h的误差只与$loss_{\tau}$有关:
|
||||
|
||||
$$
|
||||
\frac{\partial Loss}{\partial s_{\tau}}=\frac{\partial loss_{\tau}}{\partial s_{\tau}} = \frac{\partial loss_{\tau}}{\partial z_{\tau}}\frac{\partial z_{\tau}}{\partial s_{\tau}} = dz_{\tau} \cdot V^{\top}
|
||||
\tag{8}
|
||||
$$
|
||||
|
||||
$$
|
||||
\begin{aligned}
|
||||
\frac{\partial Loss}{\partial h_{\tau}}&=\frac{\partial loss_{\tau}}{\partial h_{\tau}} = \frac{\partial loss_{\tau}}{\partial z_{\tau}}\frac{\partial z_{\tau}}{\partial s_{\tau}}\frac{\partial s_{\tau}}{\partial h_{\tau}} \\\\ &= dz_{\tau} \cdot V^{\top} \odot \sigma'(s_{\tau}) \rightarrow dh_{\tau}
|
||||
\end{aligned}
|
||||
\tag{9}
|
||||
$$
|
||||
|
||||
对于其它时间步来说,节点s的反向误差从红色箭头的2和横向的dh两个方向传回来,比如时间步t3:
|
||||
|
||||
$$
|
||||
\begin{aligned}
|
||||
\frac{\partial Loss}{\partial s_3}&=\frac{\partial loss_3}{\partial s_3} + \frac{\partial loss_4}{\partial h_4}\frac{\partial h_4}{\partial s_3} \\\\
|
||||
&=dz_3 \cdot V^{\top} + dh_{4} \cdot W^{\top}
|
||||
\end{aligned}
|
||||
\tag{10}
|
||||
$$
|
||||
|
||||
$$
|
||||
\begin{aligned}
|
||||
\frac{\partial Loss}{\partial h_3}&=\frac{\partial Loss}{\partial s_3}\frac{\partial s_3}{\partial h_3} \\\\
|
||||
&=(dz_3 \cdot V^{\top} + dh_4 \cdot W^{\top} ) \odot \sigma'(s_3) \rightarrow dh_3
|
||||
\end{aligned}
|
||||
\tag{11}
|
||||
$$
|
||||
|
||||
扩展到一般性:
|
||||
|
||||
$$
|
||||
\frac{\partial Loss}{\partial h_t}=(dz_t \cdot V^{\top} + dh_{t+1} \cdot W^{\top} ) \odot \sigma'(s_t) \rightarrow dh_t
|
||||
\tag{12}
|
||||
$$
|
||||
|
||||
从公式12可以看到,求任意时间步t的$dh_t$是关键的环节,有了它之后,后面的问题都和全连接网络一样了,在19.1和19.2节中也有讲述具体的方法,在此不再赘述。求$dh_t$时,是要依赖$dh_{t+1}$的结果的,所以,在通过时间的反向传播时,要先计算最后一个时间步的$dh_{\tau}$,然后按照时间倒流的顺序一步步向前推导。
|
||||
|
||||
### 19.3.3 单输出网络通过时间的反向传播
|
||||
|
||||
图19-14描述了一种通用的网络形式,即在每一个时间步都有监督学习信号,即计算损失函数值。另外一种常见的特例是,只有最后一个时间步有输出,需要计算损失函数值,并且有反向传播的梯度产生,而前面所有的其它时间步都没有输出,这种情况如图19-15所示。
|
||||
|
||||
<img src="./img/19/bptt_simple.png"/>
|
||||
|
||||
图19-15 单输出网络通过时间的反向传播
|
||||
|
||||
这种情况的反向传播比较简单,首先,最后一个时间步的梯度公式9依然不变。但是对于公式10,由于$loss_3$为0,所以公式简化为:
|
||||
|
||||
$$
|
||||
\begin{aligned}
|
||||
\frac{\partial Loss}{\partial h_3}&=\frac{\partial loss_4}{\partial h_3}
|
||||
=\frac{\partial loss_4}{\partial h_4}\frac{\partial h_4}{\partial s_3}\frac{\partial s_3}{\partial h_3} \\\\
|
||||
&=dh_{4} \cdot W^{\top} \odot \sigma'(s_3)
|
||||
\end{aligned}
|
||||
\tag{13}
|
||||
$$
|
||||
|
||||
扩展到一般性:
|
||||
|
||||
$$
|
||||
\frac{\partial Loss}{\partial h_t}=dh_{t+1} \cdot W^{\top} \odot \sigma'(s_t) \rightarrow dh_t
|
||||
\tag{14}
|
||||
$$
|
||||
|
||||
如果有4个时间步,则第一个时间步的h节点的梯度为:
|
||||
|
||||
$$
|
||||
\begin{aligned}
|
||||
\frac{\partial Loss}{\partial h_1}&=dh_{2} \cdot W^{\top} \odot \sigma'(s_1) \\\\
|
||||
&=dh_3 \cdot (W^{\top} \odot \sigma'(s_2)) \cdot (W^{\top} \odot \sigma'(s_1)) \\\\
|
||||
&=dh_4 \cdot (W^{\top} \odot \sigma'(s_3)) \cdot (W^{\top} \odot \sigma'(s_2)) \cdot (W^{\top} \odot \sigma'(s_1)) \\\\
|
||||
&=dh_4 \prod_{t=1}^3 W^{\top} \odot \sigma'(s_t)
|
||||
\end{aligned}
|
||||
$$
|
||||
|
||||
扩展到一般性:
|
||||
|
||||
$$
|
||||
\frac{\partial Loss}{\partial h_k}=dh_{\tau} \prod_{t=k}^{\tau-1} W^{\top} \odot \sigma'(s_t) \rightarrow dh_k \tag{16}
|
||||
$$
|
||||
|
||||
$$
|
||||
dh_{\tau}=dz_{\tau} \cdot V^{\top} \odot \sigma'(s_{\tau}) \tag{17}
|
||||
$$
|
||||
|
||||
公式17为最后一个时间步的梯度(公式9)的一般形式。
|
||||
|
||||
### 19.3.4 时间步类的设计
|
||||
|
||||
时间步类(timestep)的设计是核心,它体现了循环神经网络的核心概念。下面的代码是该类的初始化函数:
|
||||
|
||||
#### 初始化
|
||||
|
||||
```Python
|
||||
class timestep(object):
|
||||
def __init__(self, net_type, output_type, isFirst=False, isLast=False):
|
||||
self.isFirst = isFirst
|
||||
self.isLast = isLast
|
||||
self.netType = net_type
|
||||
if (output_type == OutputType.EachStep):
|
||||
self.needOutput = True
|
||||
elif (output_type == OutputType.LastStep and isLast):
|
||||
self.needOutput = True
|
||||
else:
|
||||
self.needOutput = False
|
||||
```
|
||||
|
||||
- isFirst和isLast参数指定了该实例是否为第一个时间步或最后一个时间步
|
||||
- netType参数指定了网络类型,回归、二分类、多分类,三种选择
|
||||
- output_type结合isLast,可以指定该时间步是否有输出,如果是最后一个时间步,肯定有输出;如果不是最后一个时间步,并且如果output_type是OutputType.EachStep,则每个时间步都有输出,否则就没有输出。最后的判断结果记录在self.needOutput上
|
||||
|
||||
#### 前向计算
|
||||
|
||||
```Python
|
||||
def forward(self, x, U, bu, V, bv, W, prev_s):
|
||||
...
|
||||
if (self.isFirst):
|
||||
self.h = np.dot(x, U) + bu
|
||||
else:
|
||||
self.h = np.dot(x, U) + np.dot(prev_s, W) + bu
|
||||
#endif
|
||||
self.s = Tanh().forward(self.h)
|
||||
if (self.needOutput):
|
||||
self.z = np.dot(self.s, V) + bv
|
||||
if (self.netType == NetType.BinaryClassifier):
|
||||
self.a = Logistic().forward(self.z)
|
||||
elif (self.netType == NetType.MultipleClassifier):
|
||||
self.a = Softmax().forward(self.z)
|
||||
else:
|
||||
self.a = self.z
|
||||
#endif
|
||||
#endif
|
||||
```
|
||||
|
||||
- 如果是第一个时间步,在计算隐层节点值时,则只需要计算np.dot(x, U),prev_s参数为None,不需要计算在内;
|
||||
- 如果不是第一个时间步,则prev_s参数是存在的,需要增加np.dot(prev_s, W)项;
|
||||
- 如果该时间步有输出要求,即self.needOutput为True,则计算输出项;
|
||||
- 如果是二分类,最后的输出用Logistic函数;如果是多分类,用Softmax函数;如果是回归,直接令self.a = self.z,这里的赋值是为了编程模型一致,对外只暴露self.a为结果值。
|
||||
|
||||
#### 反向传播
|
||||
|
||||
```Python
|
||||
def backward(self, y, prev_s, next_dh):
|
||||
if (self.isLast):
|
||||
assert(self.needOutput == True)
|
||||
self.dz = self.a - y
|
||||
self.dh = np.dot(self.dz, self.V.T) * Tanh().backward(self.s)
|
||||
self.dV = np.dot(self.s.T, self.dz)
|
||||
else:
|
||||
assert(next_dh is not None)
|
||||
if (self.needOutput):
|
||||
self.dz = self.a - y
|
||||
self.dh = (np.dot(self.dz, self.V.T) + np.dot(next_dh, self.W.T)) * Tanh().backward(self.s)
|
||||
self.dV = np.dot(self.s.T, self.dz)
|
||||
else:
|
||||
self.dz = np.zeros_like(y)
|
||||
self.dh = np.dot(next_dh, self.W.T) * Tanh().backward(self.s)
|
||||
self.dV = np.zeros_like(self.V)
|
||||
#endif
|
||||
#endif
|
||||
self.dbv = np.sum(self.dz, axis=0, keepdims=True)
|
||||
self.dbu = np.sum(self.dh, axis=0, keepdims=True)
|
||||
|
||||
self.dU = np.dot(self.x.T, self.dh)
|
||||
|
||||
if (self.isFirst):
|
||||
self.dW = np.zeros_like(self.W)
|
||||
else:
|
||||
self.dW = np.dot(prev_s.T, self.dh)
|
||||
# end if
|
||||
```
|
||||
- 如果是最后一个时间步,则肯定要有监督学习信号,因此会计算dz、dh、dV等参数,但要注意计算dh时,只有np.dot(self.dz, self.V.T)项,因为next_dh不存在;
|
||||
- 如果不是最后一个时间步,但是有输出(有监督学习信号),仍需要计算dz、dh、dV等参数,并且在计算dh时,需要考虑后一个时间步的next_dh传入,所以dh有np.dot(self.dz, self.V.T)和np.dot(next_dh, self.W.T)两部分组成;
|
||||
- 如果不是最后一个时间步,并且没有输出,则只计算dh,误差来源是后面的时间步传入的next_dh;
|
||||
- 如果是第一个时间步,则dW为0,因为prev_s为None(没有前一个时间步传入的状态值),否则需要计算dW = np.dot(prev_s.T, self.dh)。
|
||||
|
||||
### 19.3.5 网络模型类的设计
|
||||
|
||||
这部分代码量较大,由于篇幅原因,我们就不把代码全部列在这里了,而只是列出一些类方法。
|
||||
|
||||
#### 初始化
|
||||
|
||||
- 接收传入的超参数,超参数由使用者指定;
|
||||
- 创建一个子目录来保存参数初始化结果和训练结果;
|
||||
- 初始化损失函数类和训练记录类;
|
||||
- 初始化时间步实例。
|
||||
|
||||
#### 前向计算
|
||||
|
||||
循环调用所有时间步实例的前向计算方法,遵循从前向后的顺序。要注意的是prev_s变量在第一个时间步是None。
|
||||
|
||||
#### 反向传播
|
||||
|
||||
循环调用所有时间步实例的反向传播方法,遵循从后向前的顺序。要注意的是prev_s变量在第一个时间步是None,next_dh在最后一个时间步是None。
|
||||
|
||||
#### 参数更新
|
||||
|
||||
在进行完反向传播后,每个时间步都会针对各个参数有自己的误差矩阵,由于循环神经网络的参数共享特性,需要统一进行更新,即把每个时间步的误差相加,然后乘以学习率,再除以批大小。
|
||||
|
||||
以W为例,其更新过程如下:
|
||||
```Python
|
||||
dw = np.zeros_like(self.W)
|
||||
for i in range(self.ts):
|
||||
dw += self.ts_list[i].dW
|
||||
#end for
|
||||
self.W = self.W - dw * self.hp.eta / batch_size
|
||||
```
|
||||
|
||||
一定不要忘记除以批大小,笔者在开始阶段忘记了这一项,结果不得不把全局学习率设置得非常小,才可以正常训练。在加上这一项后,全局学习率设置为0.1就可以正常训练了。
|
||||
|
||||
#### 保存和加载网络参数
|
||||
|
||||
- 保存/加载初始化网络参数,为了比较不同超参对网络的影响
|
||||
- 保存/加载训练过程中最低损失函数时的网络参数
|
||||
- 保存/加载训练结束时的网络参数
|
||||
|
||||
#### 网络训练
|
||||
|
||||
代码片段如下:
|
||||
|
||||
```Python
|
||||
for epoch in range(self.hp.max_epoch):
|
||||
dataReader.Shuffle()
|
||||
for iteration in range(max_iteration):
|
||||
# get data
|
||||
batch_x, batch_y = GetBatchTrainSamples()
|
||||
# forward
|
||||
self.forward(batch_x)
|
||||
# backward
|
||||
self.backward(batch_y)
|
||||
# update
|
||||
self.update(batch_x.shape[0])
|
||||
# check loss and accuracy
|
||||
if (checkpoint):
|
||||
loss,acc = self.check_loss(X,Y)
|
||||
```
|
||||
- 外循环控制训练的epoch数,并且在每个epoch之间打乱样本顺序
|
||||
- 内循环控制一个epoch内的迭代次数
|
||||
- 每次迭代都遵守前向计算、反向传播、更新参数的顺序
|
||||
- 定期检查验证集的损失函数值和准确度值
|
||||
|
||||
### 代码位置
|
||||
|
||||
|
|
|
@ -3,7 +3,8 @@
|
|||
|
||||
## 19.4 实现空气质量预测
|
||||
|
||||
在19.3节中搭建了一个通用的循环神经网络模型,现在看看如何把这个模型应用到实际中。
|
||||
以下为本小节目录,详情请参阅《智能之门》正版图书,高等教育出版社。
|
||||
|
||||
|
||||
### 19.4.1 提出问题
|
||||
|
||||
|
@ -13,262 +14,11 @@
|
|||
|
||||
### 19.4.2 准备数据
|
||||
|
||||
#### 数据准备
|
||||
|
||||
北京空气质量数据来源于此:"https://archive.ics.uci.edu/ml/machine-learning-databases/00381/",下载 PRSA_data_2010.1.1-2014.12.31.csv,下载后把它改名为 PM25_data.csv,拷贝到 "ch19-RNNBasic\ExtendedDataReader\data\" 目录下。
|
||||
|
||||
然后运行 ch19_PM25_data.py,将会在上述目录下生成两个文件:
|
||||
|
||||
- ch19_pm25_train.npz,训练数据
|
||||
- ch19_pm25.test.npz, 测试数据
|
||||
|
||||
#### 原始数据格式
|
||||
|
||||
它的原始数据格式见表19-6。
|
||||
|
||||
表19-6 空气质量数据字段说明
|
||||
|
||||
|字段序号|英文名称|中文名称|取值说明|
|
||||
|---|---|---|---|
|
||||
|1| No |行数|1~43824|
|
||||
|2| year|年|2010~2014|
|
||||
|3| month|月|1~12|
|
||||
|4| day|日|1~31|
|
||||
|5| hour|小时|0~23|
|
||||
|6| pm2.5|PM2.5浓度|0~994|
|
||||
|7| DEWP|露点|-40~28|
|
||||
|8| TEMP|温度|-19~42|
|
||||
|9| PRES|气压|991~1046|
|
||||
|10| cbwd|风向|cv,NE,NW,SE|
|
||||
|11| lws|累积风速|0.45~585.6|
|
||||
|12| ls|累积降雪量|0~27|
|
||||
|13| lr|累积降雨量|0~36|
|
||||
|
||||
注意数据中最后三个字段都是按小时统计的累积量,以下面的数据片段为例:
|
||||
|
||||
```
|
||||
No. 年 月 日 时 污染 露点 温 气压 风向 风速 雪 雨
|
||||
-------------------------------------------------------------
|
||||
...
|
||||
22 2010 1 1 21 NA -17 -5 1018 NW 1.79 0 0
|
||||
23 2010 1 1 22 NA -17 -5 1018 NW 2.68 0 0
|
||||
24 2010 1 1 23 NA -17 -5 1020 cv 0.89 0 0
|
||||
25 2010 1 2 0 129 -16 -4 1020 SE 1.79 0 0
|
||||
26 2010 1 2 1 148 -15 -4 1020 SE 2.68 0 0
|
||||
27 2010 1 2 2 159 -11 -5 1021 SE 3.57 0 0
|
||||
28 2010 1 2 3 181 -7 -5 1022 SE 5.36 1 0
|
||||
29 2010 1 2 4 138 -7 -5 1022 SE 6.25 2 0
|
||||
30 2010 1 2 5 109 -7 -6 1022 SE 7.14 3 0
|
||||
31 2010 1 2 6 105 -7 -6 1023 SE 8.93 4 0
|
||||
32 2010 1 2 7 124 -7 -5 1024 SE 10.72 0 0
|
||||
...
|
||||
```
|
||||
|
||||
第22行数据和第23行数据的风向都是NW(西北风),前者的风速为1.79米/秒,后者的数值是2.68,但是应该用$2.68-1.79=0.89$米/秒,因为2.68是累积值,表示这两个小时一直是西北风。第24行数据的风向是cv,表示风力很小且风向不明显,正处于交替阶段。可以看到第25行数据的风向就变成了SE(东南风),再往后又是持续的东南风数值的累积。
|
||||
|
||||
降雪量也是如此,比如第28行数据,开始降雪,到第31行数据结束,数值表示持续降雪的时长,单位为小时。
|
||||
|
||||
#### 累积值的处理
|
||||
|
||||
前面说过了,风速是累积值,这种累积关系实际上与循环神经网络的概念是重合的,因为循环神经网络设计的目的就是要识别这种与时间相关的特征,所以,我们需要把数据还原,把识别特征的任务交给循环神经网络来完成,而不要人为地制造特征。
|
||||
|
||||
假设原始数据为:
|
||||
|
||||
```
|
||||
24 cv 0.89
|
||||
25 SE 1.79
|
||||
26 SE 2.68
|
||||
27 SE 3.57
|
||||
28 SE 5.36
|
||||
29 SE 6.25
|
||||
```
|
||||
则去掉累积值之后的记录为:
|
||||
```
|
||||
24 cv 0.89
|
||||
25 SE 1.79
|
||||
26 SE 0.89(=2.68-1.79)
|
||||
27 SE 0.89(=3.57-2.68)
|
||||
28 SE 1.79(=5.36-3.57)
|
||||
29 SE 0.89(=6.25-5.36)
|
||||
```
|
||||
所以在处理数据时要注意把累积值变成当前值,需要用当前行的数值减去上一行的数值。
|
||||
|
||||
#### 缺失值的处理
|
||||
|
||||
大致浏览一下原始数据,可以看到PM2.5字段有不少缺失值,比如前24条数据中,该字段就是NA。在后面的数据中,还有很多段是NA的情况。
|
||||
|
||||
很多资料建议对于缺失值的处理建议是删除该记录或者填充为0,但是在本例中都不太合适。
|
||||
- 删除记录,会造成训练样本的不连续,而循环神经网络的通常要求时间步较长,这样遇到删除的记录时会跳到后面的记录。假设时间步为4,应该形成的记录是[1,2,3,4],如果3、4记录被删除,会变成[1,2,5,6],从时间上看没有真正地连续,会给训练带来影响。
|
||||
- 如果填充为0,相当于标签值PM2.5数据为0,会给训练带来更大的影响。
|
||||
|
||||
所以,这两种方案我们都不能采用。在不能完全还原原始数据的情况下,我们采用折中的插值法来补充缺失字段。
|
||||
|
||||
假设有PM2.5的字段记录如下:
|
||||
|
||||
```
|
||||
1 14.5
|
||||
2 NA
|
||||
3 NA
|
||||
4 NA
|
||||
5 NA
|
||||
6 20.7
|
||||
```
|
||||
中间缺失了4个记录,采用插值法,插值=$(20.7-14.5)/(6-1)=1.24$,则数据变为:
|
||||
```
|
||||
1 14.5
|
||||
2 15.74(=14.5+1.24)
|
||||
3 16.98(=15.74+1.24)
|
||||
4 18.22(=16.98+1.24)
|
||||
5 19.46(=18.22+1.24)
|
||||
6 20.7
|
||||
```
|
||||
|
||||
#### 无用特征的处理
|
||||
|
||||
1. 序号肯定没用,因为是人为给定的
|
||||
2. 年份字段也没用,除非你认为污染值是每年都会变得更糟糕
|
||||
3. 雨和雪的气象条件,对于PM2.5来说是没有用的,因为主要是温度、湿度、风向决定了雨雪的成因,而降水本身不会改变PM2.5的数值
|
||||
4. “月日时”三个字段是否有用呢?实际上“月日”代表了季节,明显的特征是温度、气压等;“时”代表了一天的温度变化。所以有了温度、气压,就没有必要有“日月时”了
|
||||
|
||||
对于以上的无用特征值,要把该字段从数据中删除。如果不删除,网络训练也可以进行,造成的影响会是:
|
||||
|
||||
1. 需要更多的隐层神经元
|
||||
2. 需要更长的计算时间
|
||||
|
||||
#### 预测类型
|
||||
|
||||
预测,可以是预测PM2.5的具体数值,也可以是预测空气质量的好坏程度,按照标准,我们可以把PM2.5的数值分为以下6个级别,如表19-7所示。
|
||||
|
||||
表19-7 PM2.5数值及级别对应
|
||||
|
||||
|级别|数值|
|
||||
|---|---|
|
||||
|0|0~50|
|
||||
|1|50~100|
|
||||
|2|100~150|
|
||||
|3|150~200|
|
||||
|4|200~300|
|
||||
|5|300以上|
|
||||
|
||||
如果预测具体数值,则是个回归网络,需要在最后一个时间步上用线性网络,后接一个均方差损失函数;如果预测污染级别,则是个分类网络,需要在最后一个时间步上用Softmax做6分类,后接一个交叉熵损失函数。
|
||||
|
||||
由于我们在19.3节实现了一个“通用的循环神经网络”,所以这些细节就可以不考虑了,只需要指定网络类型为NetType.Fitting或者NetType.MultipleClassifier即可。
|
||||
|
||||
#### 对于PM2.5数值字段的使用
|
||||
|
||||
在前面的前馈神经网络的学习中,我们知道在本问题中,PM2.5的数值应该作为标签值,那么它就不应该出现在训练样本TrainX中,而是只在标签值TrainY中出现。
|
||||
|
||||
到了循环神经网络的场景,很多情况下,需要前面时间步的所有信息才能预测下一个时间步的数值,比如股票的股价预测,股价本身是要本预测的标签值,但是如果没有前一天的股价作为输入,是不可能预测出第二天的股价的。所以,在这里股价既是样本值,又是标签值。
|
||||
|
||||
在这个PM2.5的例子中,也是同样的情况,前一时刻的污染数值必须作为输入值,来预测下一时刻的污染数值。笔者曾经把PM2.5数值从TrainX中删除,试图直接训练出一个拟合网络,但是其准确度非常的低,一度令笔者迷惑,还以为是循环神经网络的实现代码有问题。后来想通了这一点,把PM2.5数值加入到了训练样本,才得到了比较满意的训练效果。
|
||||
|
||||
具体的用法是这样的,我们先看原始数据片段:
|
||||
|
||||
```
|
||||
No. 污染 露点 温 气压 风向 风速
|
||||
-------------------------------------------------------------
|
||||
...
|
||||
25 129 -16 -4 1020 SE 1.79
|
||||
26 148 -15 -4 1020 SE 2.68
|
||||
27 159 -11 -5 1021 SE 3.57
|
||||
28 181 -7 -5 1022 SE 5.36
|
||||
29 138 -7 -5 1022 SE 6.25
|
||||
30 109 -7 -6 1022 SE 7.14
|
||||
31 105 -7 -6 1023 SE 8.93
|
||||
32 124 -7 -5 1024 SE 10.72
|
||||
...
|
||||
```
|
||||
|
||||
这里有个问题:我们的标签值数据如何确定呢?是把污染字段数据直接拿出来就能用了吗?
|
||||
|
||||
比如第26行的污染数值为148,其含义是在当前时刻采样得到的污染数据,是第25行所描述的气象数据在持续1个小时后,在129的基础上升到了148。如果第25行的数据不是129,而是100,那么第26行的数据就可能是120,而不是148。所以,129这个数据一定要作为训练样本放入训练集中,而148是下一时刻的预测值。
|
||||
|
||||
这样就比较清楚了,我们可以处理数据如下:
|
||||
|
||||
```
|
||||
No. 污染 露点 温 气压 风向 风速 标签值Y
|
||||
-------------------------------------------------------------
|
||||
...
|
||||
25 129 -16 -4 1020 SE 1.79 148
|
||||
26 148 -15 -4 1020 SE 2.68 159
|
||||
27 159 -11 -5 1021 SE 3.57 181
|
||||
28 181 -7 -5 1022 SE 5.36 138
|
||||
29 138 -7 -5 1022 SE 6.25 109
|
||||
30 109 -7 -6 1022 SE 7.14 105
|
||||
31 105 -7 -6 1023 SE 8.93 124
|
||||
32 124 -7 -5 1024 SE 10.72 134
|
||||
...
|
||||
```
|
||||
仔细对比数据,其实就是把污染字段的数值向上移一个时间步,就可以作为标签值。
|
||||
|
||||
如果想建立一个4个时间步的训练集,那么数据会是这样的:
|
||||
|
||||
```
|
||||
No. 污染 露点 温 气压 风向 风速 标签值Y
|
||||
-------------------------------------------------------------
|
||||
(第一个样本,含有4个时间步)
|
||||
25 129 -16 -4 1020 SE 1.79
|
||||
26 148 -15 -4 1020 SE 2.68
|
||||
27 159 -11 -5 1021 SE 3.57
|
||||
28 181 -7 -5 1022 SE 5.36 138
|
||||
(第二个样本,含有4个时间步)
|
||||
29 138 -7 -5 1022 SE 6.25
|
||||
30 109 -7 -6 1022 SE 7.14
|
||||
31 105 -7 -6 1023 SE 8.93
|
||||
32 124 -7 -5 1024 SE 10.72 134
|
||||
...
|
||||
```
|
||||
该样本是个三维数据,第一维是样本序列,第二维是时间步,第三维是气象条件。针对每个样本只有一个标签值,而不是4个。第一个样本的标签值138,实际上是原始数据第28行的PM2.5数据;第二个样本的标签值134,是原始数据第33行的PM2.5数据。
|
||||
|
||||
如果是预测污染级别,则把PM2.5数据映射到0~5的六个级别上即可,标签值同样也是级别数据。
|
||||
|
||||
### 19.4.3 训练一个回归预测网络
|
||||
|
||||
下面我们训练一个回归网络,预测具体的PM2.5数值,由于有19.3节的代码支持,只需要在19.4节的代码上改一行就可以了:
|
||||
|
||||
```Python
|
||||
net_type = NetType.Fitting
|
||||
```
|
||||
|
||||
图19-16显示了训练过程,一开始变化得很快,然后变得很平缓。
|
||||
|
||||
<img src="./img/19/pm25_fitting_loss_24.png"/>
|
||||
|
||||
图19-16 训练过程中的损失函数值和准确度的变化
|
||||
|
||||
然后分别预测了未来8、4、2、1小时的污染数值,并截取了中间一小段数据来展示预测效果,如表19-8所示。
|
||||
|
||||
表19-8 预测时长与准确度的关系
|
||||
|
||||
|预测时长|结果|预测结果|
|
||||
|---|---|---|
|
||||
|8小时|损失函数值:<br/>0.001171<br/>准确率:<br/>0.737769|<img src="./img/19/pm25_fitting_result_24_8.png" height="240"/>|
|
||||
|4小时|损失函数值:<br/>0.000686<br/>准确率:<br/>0.846447|<img src="./img/19/pm25_fitting_result_24_4.png" height="240"/>|
|
||||
|2小时|损失函数值:<br/>0.000414<br/>准确率:<br/>0.907291|<img src="./img/19/pm25_fitting_result_24_2.png" height="240"/>|
|
||||
|1小时|损失函数值:<br/>0.000268<br/>准确率:<br/>0.940090|<img src="./img/19/pm25_fitting_result_24_1.png" height="240"/>|
|
||||
|
||||
从上面的4张图的直观比较可以看出来,预测时间越短的越准确,预测8小时候的污染数据的准确度是72%,而预测1小时的准确度可以达到94%。
|
||||
|
||||
### 19.4.4 几个预测时要注意的问题
|
||||
|
||||
#### 准确度问题
|
||||
|
||||
预测8小时的具体污染数值的准确度是73%,而按污染程度做的分类预测的准确度为60%,为什么有差异呢?
|
||||
|
||||
在训练前,一般都需要把数据做归一化处理,因此PM2.5的数值都被归一到[0,1]之间。在预测污染数值时,我们并没有把预测值还原为真实值。假设预测值为0.11,标签值为0.12,二者相差0.01。但是如果都还原为真实值的话,可能会是110和120做比较,这样差别就比较大了。
|
||||
|
||||
#### 预测方法
|
||||
|
||||
以预测4小时为例,具体的方法用图19-17可以示意性地解释。
|
||||
|
||||
<img src="./img/19/pm25_4_pred.png"/>
|
||||
|
||||
图19-17 预测未来4个时间步的示意图
|
||||
|
||||
1. $a,b,c,d$为到当前为止前4个时间步的记录,用它预测出了第5个时间步的情况$w$,并在第二次预测时加入到预测输入部分,挤掉最前面的$a$;
|
||||
2. 用$b,c,d,w$预测出$x$;
|
||||
3. 用$c,d,w,x$预测出$y$;
|
||||
4. 用$d,w,x,y$预测出$z$。
|
||||
|
||||
### 代码位置
|
||||
|
||||
|
|
|
@ -3,7 +3,8 @@
|
|||
|
||||
## 19.5 不定长时序的循环神经网络
|
||||
|
||||
本小节中,我们将学习具有不固定的时间步的循环神经网络网络,用于多分类功能。
|
||||
以下为本小节目录,详情请参阅《智能之门》正版图书,高等教育出版社。
|
||||
|
||||
|
||||
### 19.5.1 提出问题
|
||||
|
||||
|
@ -34,140 +35,14 @@ Hitomi Japanese
|
|||
|
||||
### 19.5.2 准备数据
|
||||
|
||||
#### 下载数据
|
||||
|
||||
从这里 "https://download.pytorch.org/tutorial/data.zip" 下载数据到本地,然后解压缩出其中的 "data\names\" 目录下的所有文件到 "ch19-RNNBasic\ExtendedDataReader\data\names" 中。
|
||||
|
||||
然后运行 ch19_NameClassifier_data.py,将会在 "ch19-RNNBasic\ExtendedDataReader\data\" 目录中生成一个文件:ch19.name_language.txt。
|
||||
|
||||
|
||||
|
||||
循环神经网络的要点是“循环”二字,也就是说一个样本中的数据要分成连续若干个时间步,然后逐个“喂给”网络进行训练。如果两个样本的时间步总数不同,是不能做为一个批量一起喂给网络的,比如一个名字是Rong,另一个名字是Aggio,这两个名字不能做为一批计算。
|
||||
|
||||
在本例中,由于名字的长度不同,所以不同长度的两个名字,是不能放在一个batch里做批量运算的。但是如果一个一个地训练样本,将会花费很长的时间,所以需要我们对本例中的数据做一个特殊的处理:
|
||||
|
||||
1. 先按字母个数(名字的长度)把所有数据分开,由于最短的名字是2个字母,最长的是19个字母,所以一共应该有18组数据(实际上只有15组,中间有些长度的名字不存在)。
|
||||
2. 使用OneHot编码把名字转换成向量,比如:名字为“Duan”,变成小写字母“duan”,则OneHot编码是:
|
||||
|
||||
```
|
||||
[[0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], # d
|
||||
[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0], # u
|
||||
[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], # a
|
||||
[0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0]] # n
|
||||
```
|
||||
|
||||
3. 把所有相同长度的名字的OneHot编码都堆放在一个矩阵中,形成批量,这样就是成为了一个三维矩阵:
|
||||
- 第一维是名字的数量,假设一共有230个4个字母的名字,175个5个字母的名字,等等;
|
||||
- 第二维是4或者5或者其它值,即字母个数,也是时间步的个数;
|
||||
- 第三维是26,即a~z的小写字母的个数,相应的位为1,其它位为0。
|
||||
|
||||
在用SGD方法训练时,先随机选择一个组,假设是6个字母的名字,再从这一组中随机选择一个小批量,比如8个名字,这样就形成了一个8x6x26的三维批量数据。如果随机选到了7个字母的组,最后会形成8x7x26的三维批量数据。
|
||||
|
||||
### 19.5.3 搭建不定长时序的网络
|
||||
|
||||
#### 搭建网络
|
||||
|
||||
为什么是不定长时序的网络呢?因为名字的单词中的字母个数不是固定的,最少的两个字母,最多的有19个字母。
|
||||
|
||||
<img src="./img/19/name_classifier_net.png"/>
|
||||
|
||||
图19-18 不定长时间步的网络
|
||||
|
||||
在图19-18中,n=19,可以容纳19个字母的单词。为了节省空间,把最后一个时间步的y和loss画在了拐弯的位置。
|
||||
|
||||
并不是所有的时序都需要做分类输出,而是只有最后一个时间步需要。比如当名字是“guan”时,需要在第4个时序做分类输出,并加监督信号做反向传播,而前面3个时序不需要。但是当名字是“baevsky”时,需要在第7个时间步做分类输出。所以n值并不是固定的。
|
||||
|
||||
对于最后一个时间步,展开成前馈神经网络中的标准Softmax多分类。
|
||||
|
||||
#### 前向计算
|
||||
|
||||
在第19.3中已经介绍过通用的方法,所以不再赘述。本例中的特例是分类函数使用Softmax,损失函数使用多分类交叉熵函数:
|
||||
|
||||
$$
|
||||
a = Softmax(z) \tag{1}
|
||||
$$
|
||||
|
||||
$$
|
||||
Loss = loss_{\tau} = -y \odot \ln a \tag{2}
|
||||
$$
|
||||
|
||||
#### 反向传播
|
||||
|
||||
反向传播的推导和前面两节区别不大,唯一的变化是Softmax接多分类交叉熵损失函数,但这也是我们在前馈神经网络中学习过的。
|
||||
|
||||
### 19.5.4 代码实现
|
||||
|
||||
其它部分的代码都大同小异,只有主循环部分略有不同:
|
||||
|
||||
```Python
|
||||
def train(self, dataReader, checkpoint=0.1):
|
||||
...
|
||||
for epoch in range(self.hp.max_epoch):
|
||||
self.hp.eta = self.lr_decay(epoch)
|
||||
dataReader.Shuffle()
|
||||
while(True):
|
||||
batch_x, batch_y = dataReader.GetBatchTrainSamples(self.hp.batch_size)
|
||||
if (batch_x is None):
|
||||
break
|
||||
self.forward(batch_x)
|
||||
self.backward(batch_y)
|
||||
self.update()
|
||||
...
|
||||
```
|
||||
|
||||
获得批量训练数据函数,可以保证取到相同时间步的一组样本,这样就可以进行批量训练了,提高速度和准确度。如果取回None数据,说明所有样本数据都被使用过一次了,则结束本轮训练,检查损失函数值,然后进行下一个epoch的训练。
|
||||
|
||||
### 19.5.5 运行结果
|
||||
|
||||
我们需要下面一组超参来控制模型训练:
|
||||
|
||||
```Python
|
||||
eta = 0.02
|
||||
max_epoch = 100
|
||||
batch_size = 8
|
||||
num_input = dataReader.num_feature
|
||||
num_hidden = 16
|
||||
num_output = dataReader.num_category
|
||||
```
|
||||
|
||||
几个值得注意的地方是:
|
||||
|
||||
1. 学习率较大或者batch_size较小时,会造成网络不收敛,损失函数高居不下,或者来回震荡;
|
||||
2. 隐层神经元数量为16,虽然输入的x的特征值向量数为26,但却是OneHot编码,有效信息很少,所以不需要很多的神经元数量。
|
||||
|
||||
最后得到的损失函数曲线如图19-19所示。可以看到两条曲线的抖动都比较厉害,此时可以适当地降低学习率来使曲线平滑,收敛趋势稳定。
|
||||
|
||||
<img src="./img/19/name_classifier_loss.png"/>
|
||||
|
||||
图19-19 训练过程中的损失函数值和准确度的变化
|
||||
|
||||
本例没有独立的测试数据,所以最后是在训练数据上做的测试,打印输出如下所示:
|
||||
|
||||
```
|
||||
...
|
||||
99:55800:0.02 loss=0.887763, acc=0.707000
|
||||
correctness=2989/4400=0.6793181818181818
|
||||
load best parameters...
|
||||
correctness=3255/4400=0.7397727272727272
|
||||
```
|
||||
|
||||
训练100个epoch后得到的准确率为67.9%,其间我们保存了损失函数值最小的时刻的参数矩阵值,使用load best parameters方法后,再次测试,得到73.9%的准确率。
|
||||
|
||||
由于是多分类问题,所以我们尝试使用混淆矩阵的方式来分析结果。
|
||||
|
||||
表19-9
|
||||
|
||||
|最后的效果|最好的效果|
|
||||
|--|--|
|
||||
|<img src="./img/19/name_classifier_last_result.png"/>|<img src="./img/19/name_classifier_best_result.png"/>|
|
||||
|准确率为67.9%的混淆矩阵|准确率为73.9%的混淆矩阵|
|
||||
|
||||
在表19-9中的图中,对角线上的方块越亮,表示识别越准确。
|
||||
|
||||
左图,对于Dutch,被误识别为German类别的数量不少,所以Dutch-German交叉点的方块较亮,原因是German的名字确实比较多,两国的名字比较相近,使用更好的超参或更多的迭代次数可以改善。而French被识别为Irish的也比较多。
|
||||
|
||||
表19-9右图,可以看到非对角线位置的可见方块的数量明显减少,这也是准确率高的体现。
|
||||
|
||||
### 代码位置
|
||||
|
||||
ch19, Level5
|
||||
|
|
|
@ -3,238 +3,18 @@
|
|||
|
||||
## 19.6 深度循环神经网络
|
||||
|
||||
以下为本小节目录,详情请参阅《智能之门》正版图书,高等教育出版社。
|
||||
|
||||
### 19.6.1 深度循环神经网络的结构图
|
||||
|
||||
前面的几个例子中,单独看每一时刻的网络结构,其实都是由“输入层->隐层->输出层”所组成的,这与我们在前馈神经网络中学到的单隐层的知识一样,由于输入层不算做网络的一层,输出层是必须具备的,所以网络只有一个隐层。我们知道单隐层的能力是有限的,所以人们会使用更深(更多隐层)的网络来解决复杂的问题。
|
||||
|
||||
在循环神经网络中,会有同样的需求,要求每一时刻的网络是由多个隐层组成。比如图19-20为两个隐层的循环神经网络,用于解决和19.4节中的同样的问题。
|
||||
|
||||
<img src="./img/19/deep_rnn_net.png"/>
|
||||
|
||||
图19-20 两个隐层的循环神经网络
|
||||
|
||||
注意图19-20中最左侧的两个隐藏状态s1和s2是同时展开为右侧的图的,
|
||||
这样的循环神经网络称为深度循环神经网络,它可以具备比单隐层的循环神经网络更强大的能力。
|
||||
|
||||
### 19.6.2 前向计算
|
||||
|
||||
#### 公式推导
|
||||
|
||||
对于第一个时间步:
|
||||
$$
|
||||
h1 = x \cdot U \tag{1}
|
||||
$$
|
||||
$$
|
||||
h2 = s1 \cdot Q \tag{2}
|
||||
$$
|
||||
|
||||
对于后面的时间步:
|
||||
$$
|
||||
h1 = x \cdot U + s1_{t-1} \cdot W1 \tag{3}
|
||||
$$
|
||||
|
||||
$$
|
||||
h2 = s1 \cdot Q + s2_{t-1} \cdot W2 \tag{4}
|
||||
$$
|
||||
|
||||
对于所有的时间步:
|
||||
|
||||
$$
|
||||
s1 = \tanh(h1) \tag{5}
|
||||
$$
|
||||
|
||||
$$
|
||||
s2 = \tanh(h2) \tag{6}
|
||||
$$
|
||||
|
||||
对于最后一个时间步:
|
||||
$$
|
||||
z = s2 \cdot V \tag{7}
|
||||
$$
|
||||
$$
|
||||
a = Identity(z) \tag{8}
|
||||
$$
|
||||
$$
|
||||
Loss = loss_{\tau} = \frac{1}{2} (a-y)^2 \tag{9}
|
||||
$$
|
||||
|
||||
由于是拟合任务,所以公式8的Identity()函数只是简单地令a=z,以便统一编程接口,最后用均方差做为损失函数。
|
||||
|
||||
注意并不是所有的循环神经网络都只在最后一个时间步有监督学习信号,而只是我们这个问题需要这样。在19.2节中的例子就是需要在每一个时间步都要有输出并计算损失函数值的。所以,公式9中只计算了最后一个时间步的损失函数值,做为整体的损失函数值。
|
||||
|
||||
#### 代码实现
|
||||
|
||||
注意前向计算时需要把prev_s1和prev_s2传入,即上一个时间步的两个隐层的节点值(矩阵)。
|
||||
|
||||
```Python
|
||||
class timestep(object):
|
||||
def forward(self, x, U, V, Q, W1, W2, prev_s1, prev_s2, isFirst, isLast):
|
||||
...
|
||||
```
|
||||
|
||||
### 19.6.3 反向传播
|
||||
|
||||
#### 公式推导
|
||||
|
||||
反向传播部分和前面章节的内容大致相似,我们只把几个关键步骤直接列出来,不做具体推导:
|
||||
|
||||
对于最后一个时间步:
|
||||
$$
|
||||
\frac{\partial Loss}{\partial z} = a-y \rightarrow dz \tag{10}
|
||||
$$
|
||||
|
||||
$$
|
||||
\frac{\partial Loss}{\partial V}=\frac{\partial Loss}{\partial z}\frac{\partial z}{\partial V}=s2^{\top} \cdot dz \rightarrow dV \tag{11}
|
||||
$$
|
||||
|
||||
$$
|
||||
\begin{aligned}
|
||||
\frac{\partial Loss}{\partial h2} &= \frac{\partial Loss}{\partial z}\frac{\partial z}{\partial s2}\frac{\partial s2}{\partial h2}
|
||||
\\\\
|
||||
&=(dz \cdot V^{\top}) \odot \sigma'(s2) \rightarrow dh2
|
||||
\end{aligned}
|
||||
\tag{12}
|
||||
$$
|
||||
|
||||
$$
|
||||
\begin{aligned}
|
||||
\frac{\partial Loss}{\partial h1} &= \frac{\partial Loss}{\partial h2}\frac{\partial h2}{\partial s1}\frac{\partial s1}{\partial h1} \\\\
|
||||
&=(dh2 \cdot Q^{\top}) \odot \sigma'(s1) \rightarrow dh1
|
||||
\end{aligned}
|
||||
\tag{13}
|
||||
$$
|
||||
|
||||
对于其他时间步:
|
||||
|
||||
$$
|
||||
dz = 0 \tag{14}
|
||||
$$
|
||||
|
||||
$$
|
||||
\begin{aligned}
|
||||
\frac{\partial Loss}{\partial h2_t} &= \frac{\partial Loss}{\partial h2_{t+1}}\frac{\partial h2_{t+1}}{\partial s2_t}\frac{\partial s2_t}{\partial h2_t}
|
||||
\\\\
|
||||
&=(dh2_{t+1} \cdot W2^{\top}) \odot \sigma'(s2_t) \rightarrow dh2_t
|
||||
\end{aligned}
|
||||
\tag{15}
|
||||
$$
|
||||
|
||||
$$
|
||||
dV = 0 \tag{16}
|
||||
$$
|
||||
|
||||
$$
|
||||
\begin{aligned}
|
||||
\frac{\partial Loss}{\partial h1_t} &= \frac{\partial Loss}{\partial h1_{t+1}}\frac{\partial h1_{t+1}}{\partial s1_t}\frac{\partial s1_t}{\partial h1_t}+\frac{\partial loss_t}{\partial h2_t}\frac{\partial h2_t}{\partial s1_t}\frac{\partial s1_t}{\partial h1_t}
|
||||
\\\\
|
||||
&=(dh1_{t+1} \cdot W1^{\top} + dh2_t\cdot Q^{\top}) \odot \sigma'(s1_t) \rightarrow dh1_t
|
||||
\end{aligned}
|
||||
\tag{17}
|
||||
$$
|
||||
|
||||
对于第一个时间步:
|
||||
|
||||
$$
|
||||
dW1 = 0, dW2 = 0 \tag{18}
|
||||
$$
|
||||
|
||||
对于其他时间步:
|
||||
|
||||
$$
|
||||
\frac{\partial Loss}{\partial W1}=s1^{\top}_ {t-1} \cdot dh_1 \rightarrow dW1 \tag{19}
|
||||
$$
|
||||
|
||||
$$
|
||||
\frac{\partial Loss}{\partial W2}=s2^{\top}_ {t-1} \cdot dh2 \rightarrow dW2 \tag{20}
|
||||
$$
|
||||
|
||||
对于所有时间步:
|
||||
|
||||
$$
|
||||
\frac{\partial Loss}{\partial Q}=\frac{\partial Loss}{\partial h2}\frac{\partial h2}{\partial Q}=s1^{\top} \cdot dh2 \rightarrow dQ \tag{21}
|
||||
$$
|
||||
|
||||
$$
|
||||
\frac{\partial Loss}{\partial U}=\frac{\partial Loss}{\partial h1}\frac{\partial h1}{\partial U}=x^{\top} \cdot dh1 \rightarrow dU \tag{22}
|
||||
$$
|
||||
|
||||
#### 代码实现
|
||||
|
||||
```Python
|
||||
class timestep(object):
|
||||
def backward(self, y, prev_s1, prev_s2, next_dh1, next_dh2, isFirst, isLast):
|
||||
...
|
||||
```
|
||||
|
||||
### 19.6.4 运行结果
|
||||
|
||||
#### 超参设置
|
||||
|
||||
我们搭建一个双隐层的循环神经网络,隐层1的神经元数为2,隐层2的神经元数也为2,其它参数保持与单隐层的循环神经网络一致:
|
||||
|
||||
- 网络类型:回归
|
||||
- 时间步数:24
|
||||
- 学习率:0.05
|
||||
- 最大迭代数:100
|
||||
- 批大小:64
|
||||
- 输入特征数:6
|
||||
- 输出维度:1
|
||||
|
||||
#### 训练结果
|
||||
|
||||
训练过程如图19-21所示,训练结果如表19-10所示。
|
||||
|
||||
<img src="./img/19/deep_rnn_loss.png"/>
|
||||
|
||||
图19-21 训练过程中的损失函数值和准确度的变化
|
||||
|
||||
表19-10 预测时长与准确度的关系
|
||||
|
||||
|预测时长|结果|预测结果|
|
||||
|---|---|---|
|
||||
|8|损失函数值:<br/>0.001157<br/>准确度:<br/>0.740684|<img src="./img/19/deeprnn_pm25_fitting_result_24_8.png" height="240"/>
|
||||
|4|损失函数值:<br/>0.000644<br/>准确度:<br/>0.855700|<img src="./img/19/deeprnn_pm25_fitting_result_24_4.png" height="240"/>
|
||||
|2|损失函数值:<br/>0.000377<br/>准确度:<br/>0.915486|<img src="./img/19/deeprnn_pm25_fitting_result_24_2.png" height="240"/>
|
||||
|1|损失函数值:<br/>0.000239<br/>准确度:<br/>0.946411|<img src="./img/19/deeprnn_pm25_fitting_result_24_1.png" height="240"/>
|
||||
|
||||
#### 与单层循环神经网络的比较
|
||||
|
||||
对于19.3节中的单层循环神经网络,参数配置如下:
|
||||
```
|
||||
U: 6x4+4=28
|
||||
V: 4x1+1= 5
|
||||
W: 4x4 =16
|
||||
-----------
|
||||
Total: 49
|
||||
```
|
||||
|
||||
对于两层的循环神经网络来说,参数配置如下:
|
||||
|
||||
```
|
||||
U: 6x2=12
|
||||
Q: 2x2= 4
|
||||
V: 2x1= 2
|
||||
W1:2x2= 4
|
||||
W2:2x2= 4
|
||||
---------
|
||||
Total: 26
|
||||
```
|
||||
|
||||
表19-11 预测结果比较
|
||||
|
||||
||单隐层循环神经网络|深度(双层)循环神经网络|
|
||||
|---|---|---|
|
||||
|参数个数|49|26|
|
||||
|损失函数值(8小时)|0.001171|0.001157|
|
||||
|损失函数值(4小时)|0.000686|0.000644|
|
||||
|损失函数值(2小时)|0.000414|0.000377|
|
||||
|损失函数值(1小时)|0.000268|0.000239|
|
||||
|准确率值(8小时)|0.737769|0.740684|
|
||||
|准确率值(4小时)|0.846447|0.855700|
|
||||
|准确率值(2小时)|0.907291|0.915486|
|
||||
|准确率值(1小时)|0.940090|0.946411|
|
||||
|
||||
从表19-11可以看到,双层的循环神经网络在参数少的情况下,取得了比单层循环神经网络好的效果。
|
||||
|
||||
### 代码位置
|
||||
|
||||
|
|
|
@ -3,269 +3,17 @@
|
|||
|
||||
## 19.7 双向循环神经网络
|
||||
|
||||
以下为本小节目录,详情请参阅《智能之门》正版图书,高等教育出版社。
|
||||
|
||||
### 19.7.1 深度循环神经网络的结构图
|
||||
|
||||
前面学习的内容,都是因为“过去”的时间步的状态对“未来”的时间步的状态有影响,在本节中,我们将学习一种双向影响的结构,即双向循环神经网络。
|
||||
|
||||
比如在一个语音识别的模型中,可能前面的一个词听上去比较模糊,会产生多个猜测,但是后面的词都很清晰,于是可以用后面的词来为前面的词提供一个最有把握(概率最大)的猜测。再比如,在手写识别应用中,前面的笔划与后面的笔划是相互影响的,特别是后面的笔划对整个字的识别有较大的影响。
|
||||
|
||||
在本节中会出现两组相似的词:前向计算、反向传播、正向循环、逆向循环。区别如下:
|
||||
|
||||
- 前向计算:是指神经网络中通常所说的前向计算,包括正向循环的前向计算和逆向循环的前向计算。
|
||||
- 反向传播:是指神经网络中通常所说的反向传播,包括正向循环的反向传播和逆向循环的反向传播。
|
||||
- 正向循环:是指双向循环神经网络中的从左到右时间步。在正向过程中,会存在前向计算和反向传播。
|
||||
- 逆向循环:是指双向循环神经网络中的从右到左时间步。在逆向过程中,也会存在前向计算和反向传播。
|
||||
|
||||
很多资料中关于双向循环神经网络的示意如图19-22所示。
|
||||
|
||||
<img src="./img/19/bi_rnn_net_wrong.png"/>
|
||||
|
||||
图19-22 双向循环神经网络结构图(不正确)
|
||||
|
||||
在图19-22中,$h_{tn}$中的n表示时间步,在图中取值为1至4。
|
||||
- $h_{t1}$至$h_{t4}$是正向循环的四个隐层状态值,$U$、$V$、$W$ 分别是它们的权重矩阵值;
|
||||
- $h'_ {t1}$至$h'_ {t4}$是逆向循环的四个隐层状态值,$U'$、$V'$、 $W'$ 分别是它们的权重矩阵值;
|
||||
- $S_{t1}$至$S_{t4}$是正逆两个方向的隐层状态值的和。
|
||||
|
||||
但是,请大家记住,图19-22和上面的相关解释是不正确的!主要的问题集中在 $s_t$ 是如何生成的。
|
||||
|
||||
$h_t$ 和 $h'_ t$ 到$s_t$之间不是矩阵相乘的关系,所以没有 $V$ 和 $V'$ 这两个权重矩阵。
|
||||
|
||||
正向循环的最后一个时间步$h_{t4}$和逆向循环的第一个时间步$h_{t4}'$共同生成$s_{t4}$,这也是不对的。因为对于正向循环来说,用 $h_{t4}$ 没问题。但是对于逆向循环来说,$h'_ {t4}$ 只是第一个时间步的结果,后面的计算还未发生,所以 $h'_{t4}$ 非常不准确。
|
||||
|
||||
正确的双向循环神经网络图应该如图19-23所示。
|
||||
|
||||
<img src="./img/19/bi_rnn_net_right.png"/>
|
||||
|
||||
图19-23 双向循环神经网络结构图
|
||||
|
||||
用$h1/s1$表示正向循环的隐层状态,$U1$、$W1$表示权重矩阵;用$h2/s2$表示逆向循环的隐层状态,$U2$、$W2$表示权重矩阵。$s$ 是 $h$ 的激活函数结果。
|
||||
|
||||
请注意上下两组$x_{t1}$至$x_{t4}$的顺序是相反的:
|
||||
|
||||
- 对于正向循环的最后一个时间步来说,$x_{t4}$ 作为输入,$s1_{t4}$是最后一个时间步的隐层值;
|
||||
- 对于逆向循环的最后一个时间步来说,$x_{t1}$ 作为输入,$s2_{t4}$是最后一个时间步的隐层值;
|
||||
- 然后 $s1_{t4}$ 和 $s2_{t4}$ 拼接得到 $s_{t4}$,再通过与权重矩阵 $V$ 相乘得出 $Z$。
|
||||
|
||||
这就解决了图19-22中的逆向循环在第一个时间步的输出不准确的问题,对于两个方向的循环,都是用最后一个时间步的输出。
|
||||
|
||||
图19-23中的 $s$ 节点有两种,一种是绿色实心的,表示有实际输出;另一种是绿色空心的,表示没有实际输出,对于没有实际输出的节点,也不需要做反向传播的计算。
|
||||
|
||||
如果需要在每个时间步都有输出,那么图19-23也是一种合理的结构,而图19-22就无法解释了。
|
||||
|
||||
### 19.7.2 前向计算
|
||||
|
||||
我们先假设应用场景只需要在最后一个时间步有输出(比如19.4节和19.5节中的应用就是如此),所以t2所代表的所有中间步都没有a、loss、y三个节点(用空心的圆表示),只有最后一个时间步有输出。
|
||||
|
||||
与前面的单向循环网络不同的是,由于有逆向网络的存在,在逆向过程中,t3是第一个时间步,t1是最后一个时间步,所以t1也应该有输出。
|
||||
|
||||
#### 公式推导
|
||||
|
||||
$$
|
||||
h1 = x \cdot U1 + s1_{t-1} \cdot W1 \tag{1}
|
||||
$$
|
||||
|
||||
注意公式1在t1时,$s1_{t-1}$是空,所以加法的第二项不存在。
|
||||
|
||||
$$
|
||||
s1 = Tanh(h1) \tag{2}
|
||||
$$
|
||||
|
||||
$$
|
||||
h2 = x \cdot U2 + s2_{t-1} \cdot W2 \tag{3}
|
||||
$$
|
||||
|
||||
注意公式3在t1时,$s2_{t-1}$是空,所以加法的第二项不存在。而且 $x$ 是颠倒时序后的值。
|
||||
|
||||
$$
|
||||
s2 = Tanh(h2) \tag{4}
|
||||
$$
|
||||
|
||||
$$
|
||||
s = s1 \oplus s2 \tag{5}
|
||||
$$
|
||||
|
||||
公式5有几种实现方式,比如sum(矩阵求和)、concat(矩阵拼接)、mul(矩阵相乘)、ave(矩阵平均),我们在这里使用矩阵求和,这样在反向传播时的公式比较容易推导。
|
||||
|
||||
$$
|
||||
z = s \cdot V \tag{6}
|
||||
$$
|
||||
|
||||
$$
|
||||
a = Softmax(z) \tag{7}
|
||||
$$
|
||||
|
||||
公式4、5、6、7只在最后一个时间步发生。
|
||||
|
||||
#### 代码实现
|
||||
|
||||
由于是双向的,所以在主过程中,存在一正一反两个计算链,1表示正向,2表示逆向,3表示输出时的计算。
|
||||
|
||||
```Python
|
||||
class timestep(object):
|
||||
def forward_1(self, x1, U1, bU1, W1, prev_s1, isFirst):
|
||||
...
|
||||
|
||||
def forward_2(self, x2, U2, bU2, W2, prev_s2, isFirst):
|
||||
...
|
||||
|
||||
def forward_3(self, V, bV, isLast):
|
||||
...
|
||||
```
|
||||
|
||||
### 19.7.3 反向传播
|
||||
|
||||
#### 正向循环的反向传播
|
||||
|
||||
先推导正向循环的反向传播公式,即关于h1、s1节点的计算。
|
||||
|
||||
对于最后一个时间步(即$\tau$):
|
||||
|
||||
$$
|
||||
\frac{\partial Loss}{\partial z_\tau} = \frac{\partial loss_\tau}{\partial z_\tau}=a_\tau-y_\tau \rightarrow dz_\tau \tag{8}
|
||||
$$
|
||||
|
||||
对于其它时间步来说$dz_t=0$,因为不需要输出。
|
||||
|
||||
因为$s=s1 + s2$,所以$\frac{\partial s}{\partial s1}=1$,代入下面的公式中:
|
||||
|
||||
$$
|
||||
\begin{aligned}
|
||||
\frac{\partial Loss}{\partial h1_\tau}&=\frac{\partial loss_\tau}{\partial h1_\tau}=\frac{\partial loss_\tau}{\partial z_\tau}\frac{\partial z_\tau}{\partial s_\tau}\frac{\partial s_\tau}{\partial s1_\tau}\frac{\partial s1_\tau}{\partial h1_\tau} \\\\
|
||||
&=dz_\tau \cdot V^T \odot \sigma'(s1_\tau) \rightarrow dh1_\tau
|
||||
\end{aligned}
|
||||
\tag{9}
|
||||
$$
|
||||
|
||||
其中,下标$\tau$表示最后一个时间步,$\sigma'(s1)$表示激活函数的导数,$s1$是激活函数的数值。下同。
|
||||
|
||||
比较公式9和19.3节通用循环神经网络模型中的公式9,形式上是完全相同的,原因是$\frac{\partial s}{\partial s1}=1$,并没有给我们带来任何额外的计算,所以关于其他时间步的推导也应该相同。
|
||||
|
||||
对于中间的所有时间步,除了本时间步的$loss_t$回传误差外,后一个时间步的$h1_{t+1}$也会回传误差:
|
||||
|
||||
$$
|
||||
\begin{aligned}
|
||||
\frac{\partial Loss}{\partial h1_t} &= \frac{\partial loss_t}{\partial z_t}\frac{\partial z_t}{\partial s_t}\frac{\partial s_t}{\partial s1_t}\frac{\partial s1_t}{\partial h1_t} + \frac{\partial Loss}{\partial h1_{t+1}}\frac{\partial h1_{t+1}}{\partial s1_{t}}\frac{\partial s1_t}{\partial h1_t}
|
||||
\\\\
|
||||
&=dz_t \cdot V^{\top} \odot \sigma'(s1_t) + \frac{\partial Loss}{\partial h1_{t+1}} \cdot W1^{\top} \odot \sigma'(s1_t)
|
||||
\\\\
|
||||
&=(dz_t \cdot V^{\top} + dh1_{t+1} \cdot W1^{\top}) \odot \sigma'(s1_t) \rightarrow dh1_t
|
||||
\end{aligned} \tag{10}
|
||||
$$
|
||||
|
||||
公式10中的$dh1_{t+1}$,就是上一步中计算得到的$dh1_t$,如果追溯到最开始,即公式9中的$dh1_\tau$。因此,先有最后一个时间步的$dh1_\tau$,然后依次向前推,就可以得到所有时间步的$dh1_t$。
|
||||
|
||||
对于$V$来说,只有当前时间步的损失函数会给它反向传播的误差,与别的时间步没有关系,所以有:
|
||||
|
||||
$$
|
||||
\frac{\partial loss_t}{\partial V_t} = \frac{\partial loss_t}{\partial z_t}\frac{\partial z_t}{\partial V_t}= s_t^{\top} \cdot dz_t \rightarrow dV_t \tag{11}
|
||||
$$
|
||||
|
||||
对于$U1$,后面的时间步都会给它反向传播误差,但是我们只从$h1$节点考虑:
|
||||
|
||||
$$
|
||||
\frac{\partial Loss}{\partial U1_t} = \frac{\partial Loss}{\partial h1_t}\frac{\partial h1_t}{\partial U1_t}= x^{\top}_ t \cdot dh1_t \rightarrow dU1_t \tag{12}
|
||||
$$
|
||||
|
||||
对于$W1$,和$U1$的考虑是一样的,只从当前时间步的$h1$节点考虑:
|
||||
|
||||
$$
|
||||
\frac{\partial Loss}{\partial W1_t} = \frac{\partial Loss}{\partial h1_t}\frac{\partial h1_t}{\partial W1_t}= s1_{t-1}^{\top} \cdot dh1_t \rightarrow dW1_t \tag{13}
|
||||
$$
|
||||
|
||||
对于第一个时间步,$s1_{t-1}$不存在,所以没有$dW1$:
|
||||
|
||||
$$
|
||||
dW1 = 0 \tag{14}
|
||||
$$
|
||||
|
||||
#### 逆向循环的反向传播
|
||||
|
||||
逆向循环的反向传播和正向循环一模一样,只是把 $1$ 变成 $2$ 即可,比如公式13变成:
|
||||
|
||||
$$
|
||||
\frac{\partial Loss}{\partial W2_t} = \frac{\partial Loss}{\partial h2_t}\frac{\partial h2_t}{\partial W2_t}= s2_{t-1}^{\top} \cdot dh2_t \rightarrow dW2_t
|
||||
$$
|
||||
|
||||
### 19.7.4 代码实现
|
||||
|
||||
#### 单向循环神经网络的效果
|
||||
|
||||
为了与单向的循环神经网络比较,笔者在Level3_Base的基础上实现了一个MNIST分类,超参如下:
|
||||
|
||||
```Python
|
||||
net_type = NetType.MultipleClassifier # 多分类
|
||||
output_type = OutputType.LastStep # 只在最后一个时间步输出
|
||||
num_step = 28
|
||||
eta = 0.005 # 学习率
|
||||
max_epoch = 100
|
||||
batch_size = 128
|
||||
num_input = 28
|
||||
num_hidden = 32 # 隐层神经元32个
|
||||
num_output = 10
|
||||
```
|
||||
|
||||
得到的训练结果如下:
|
||||
|
||||
```
|
||||
...
|
||||
99:42784:0.005000 loss=0.212298, acc=0.943200
|
||||
99:42999:0.005000 loss=0.200447, acc=0.947200
|
||||
save last parameters...
|
||||
testing...
|
||||
loss=0.186573, acc=0.948800
|
||||
load best parameters...
|
||||
testing...
|
||||
loss=0.176821, acc=0.951700
|
||||
```
|
||||
|
||||
最好的时间点的权重矩阵参数得到的准确率为95.17%,损失函数值为0.176821。
|
||||
|
||||
#### 双向循环神经网络的效果
|
||||
```Python
|
||||
eta = 0.01
|
||||
max_epoch = 100
|
||||
batch_size = 128
|
||||
num_step = 28
|
||||
num_input = 28
|
||||
num_hidden1 = 20 # 正向循环隐层神经元20个
|
||||
num_hidden2 = 20 # 逆向循环隐层神经元20个
|
||||
num_output = 10
|
||||
```
|
||||
|
||||
得到的结果如图19-23所示。
|
||||
|
||||
<img src="./img/19/bi_rnn_loss.png"/>
|
||||
|
||||
图19-23 训练过程中损失函数值和准确率的变化
|
||||
|
||||
下面是打印输出:
|
||||
|
||||
```
|
||||
...
|
||||
save best parameters...
|
||||
98:42569:0.002000 loss=0.163360, acc=0.955200
|
||||
99:42784:0.002000 loss=0.164529, acc=0.954200
|
||||
99:42999:0.002000 loss=0.163679, acc=0.955200
|
||||
save last parameters...
|
||||
testing...
|
||||
loss=0.144703, acc=0.958000
|
||||
load best parameters...
|
||||
testing...
|
||||
loss=0.146799, acc=0.958000
|
||||
```
|
||||
|
||||
最好的时间点的权重矩阵参数得到的准确率为95.59%,损失函数值为0.153259。
|
||||
|
||||
#### 比较
|
||||
|
||||
表19-12 单向和双向循环神经网络的比较
|
||||
|
||||
||单向|双向|
|
||||
|---|---|---|
|
||||
|参数个数|2281|2060|
|
||||
|准确率|95.17%|95.8%|
|
||||
|损失函数值|0.176|0.144|
|
||||
|
||||
### 代码位置
|
||||
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
|
||||
## 20.1 LSTM基本原理
|
||||
|
||||
本小节中,我们将学习长短时记忆(Long Short Term Memory, LSTM)网络的基本原理。
|
||||
以下为本小节目录,详情请参阅《智能之门》正版图书,高等教育出版社。
|
||||
|
||||
### 20.1.1 提出问题
|
||||
|
||||
|
@ -45,331 +45,3 @@ $$
|
|||
|
||||
|
||||
### 20.1.2 LSTM网络
|
||||
|
||||
#### 20.1.2.1 LSTM的结构
|
||||
|
||||
LSTM 的设计思路比较简单,原来的RNN中隐藏层只有一个状态h,对短期输入敏感,现在再增加一个状态c,来保存长期状态。这个新增状态称为 **细胞状态(cell state)**或**单元状态**。
|
||||
增加细胞状态前后的网络对比,如图20-1,20-2所示。
|
||||
|
||||
<img src="./img/20/rnn_sketch.png" width="400" />
|
||||
|
||||
图20-1 传统RNN结构示意图
|
||||
|
||||
<img src="./img/20/lstm_sketch.png" width="400" />
|
||||
|
||||
图20-2 LSTM结构示意图
|
||||
|
||||
那么,如何控制长期状态c呢?在任意时刻$t$,我们需要确定三件事:
|
||||
|
||||
1. $t-1$时刻传入的状态$c_{t-1}$,有多少需要保留。
|
||||
2. 当前时刻的输入信息,有多少需要传递到$t+1$时刻。
|
||||
3. 当前时刻的隐层输出$h_t$是什么。
|
||||
|
||||
LSTM设计了 **门控(gate)** 结构,控制信息的保留和丢弃。LSTM有三个门,分别是:遗忘门(forget gate),输入门(input gate)和输出门(output gate)。
|
||||
|
||||
图20-3是常见的LSTM结构,我们以任意时刻t的一个LSTM单元(LSTM cell)为例,来分析其工作原理。
|
||||
|
||||
<img src="./img/20/lstm_inner_structure.png" />
|
||||
|
||||
图20-3 LSTM内部结构意图
|
||||
|
||||
#### 20.1.2.2 LSTM的前向计算
|
||||
|
||||
1. 遗忘门
|
||||
|
||||
由上图可知,遗忘门的输出为$f_t$, 采用sigmoid激活函数,将输出映射到[0,1]区间。上一时刻细胞状态$c_{t-1}$通过遗忘门时,与$f_t$结果相乘,显然,乘数为0的信息被全部丢弃,为1的被全部保留。这样就决定了上一细胞状态$c_{t-1}$有多少能进入当前状态$c_t$。
|
||||
|
||||
遗忘门$f_t$的公式如下:
|
||||
|
||||
$$
|
||||
f_t = \sigma(h_{t-1} \cdot W_f + x_t \cdot U_f + b_f) \tag{1}
|
||||
$$
|
||||
|
||||
其中,$\sigma$为sigmoid激活函数,$h_{t-1}$ 为上一时刻的隐层状态,形状为$(1 \times h)$的行向量。$x_t$为当前时刻的输入,形状为$(1 \times i)$的行向量。参数矩阵$W_f$、$U_f$分别是$(h \times h)$和$(i \times h)$的矩阵,$b_f$为$(1 \times h)$的行向量。
|
||||
|
||||
很多教科书或网络资料将公式写成如下格式:
|
||||
|
||||
$$
|
||||
f_t=\sigma(W_f\cdot[h_{t-1}, x_t] + b_f) \tag{1'}
|
||||
$$
|
||||
|
||||
或
|
||||
|
||||
$$
|
||||
\begin{aligned}
|
||||
f_t &= \sigma([W_{fh}\;W_{fx}] \begin{bmatrix}h_{t-1}\\\\x_t \end{bmatrix}+b_f) \\\\
|
||||
&= \sigma(W_{fh}h_{t-1}+W_{fx}x_t+b_f)
|
||||
\end{aligned} \tag{1''}
|
||||
$$
|
||||
|
||||
后两种形式将权重矩阵放在状态向量前面,在讲解原理时,与公式$(1)$没有区别,但在代码实现时会出现一些问题,所以,在本章中我们采用公式$(1)$的表达方式。
|
||||
|
||||
|
||||
2. 输入门
|
||||
|
||||
输入门$i_t$决定输入信息有哪些被保留,输入信息包含当前时刻输入和上一时刻隐层输出两部分,存入即时细胞状态$\tilde{c}_t$中。输入门依然采用sigmoid激活函数,将输出映射到[0,1]区间。$\tilde{c}_t$通过输入门时进行信息过滤。
|
||||
|
||||
输入门$i_t$的公式如下:
|
||||
|
||||
$$
|
||||
i_t = \sigma(h_{t-1} \cdot W_i + x_t \cdot U_i + b_i) \tag{2}
|
||||
$$
|
||||
|
||||
即时细胞状态 $\tilde{c}_ t$的公式如下:
|
||||
|
||||
$$
|
||||
\tilde c_t = \tanh(h_{t-1} \cdot W_c + x_t \cdot U_c + b_c) \tag{3}
|
||||
$$
|
||||
|
||||
上一时刻保留的信息,加上当前输入保留的信息,构成了当前时刻的细胞状态$c_t$。
|
||||
|
||||
当前细胞状态$c_t$的公式如下:
|
||||
|
||||
$$
|
||||
c_t = f_t \circ c_{t-1}+i_t \circ \tilde{c}_t \tag{4}
|
||||
$$
|
||||
|
||||
其中,符号 $\cdot$ 表示矩阵乘积, $\circ$ 表示 Hadamard 乘积,即元素乘积。
|
||||
|
||||
|
||||
3. 输出门
|
||||
|
||||
最后,需要确定输出信息。
|
||||
|
||||
输出门$o_t$决定 $h_{t-1}$ 和 $x_t$ 中哪些信息将被输出,公式如下:
|
||||
|
||||
$$
|
||||
o_t = \sigma(h_{t-1} \cdot W_o + x_t \cdot U_o + b_o) \tag{5}
|
||||
$$
|
||||
|
||||
细胞状态$c_t$通过tanh激活函数压缩到 (-1, 1) 区间,通过输出门,得到当前时刻的隐藏状态$h_t$作为输出,公式如下:
|
||||
|
||||
$$
|
||||
h_t=o_t \circ \tanh(c_t) \tag{6}
|
||||
$$s
|
||||
|
||||
最后,时刻t的预测输出为:
|
||||
|
||||
$$
|
||||
a_t = \sigma(h_t \cdot V + b) \tag{7}
|
||||
$$
|
||||
|
||||
其中,
|
||||
|
||||
$$
|
||||
z_t = h_t \cdot V + b \tag{8}
|
||||
$$
|
||||
|
||||
经过上面的步骤,LSTM就完成了当前时刻的前向计算工作。
|
||||
|
||||
#### 20.1.2.3 LSTM的反向传播
|
||||
|
||||
LSTM使用时序反向传播算法(Backpropagation Through Time, BPTT)进行计算。图20-4是带有一个输出的LSTM cell。我们使用该图来推导反向传播过程。
|
||||
|
||||
<img src="./img/20/lstm_cell.png" />
|
||||
|
||||
图20-4 带有一个输出的LSTM单元
|
||||
|
||||
假设当前LSTM cell处于第$l$层、$t$时刻。那么,它从两个方向接受反向传播的误差:一个是从$t+1$时刻$l$层传回的误差,记为$\delta^ l_{h_t}$(注意,这里的下标不是$h_{t+1}$,而是$h_t$);另一个是从$t$时刻$l+1$层的输入传回误差,记为 $\delta^ {l+1}_{x_t}$。
|
||||
|
||||
我们先复习几个在推导过程中会使用到的激活函数,以及其导数公式。令sigmoid = $\sigma$,则:
|
||||
|
||||
$$
|
||||
\sigma(z) = y = \frac{1}{1+e^{-z}} \tag{9}
|
||||
$$
|
||||
|
||||
$$
|
||||
\sigma^{\prime}(z) = y(1-y) \tag{10}
|
||||
$$
|
||||
|
||||
$$
|
||||
\tanh(z) = y = \frac{e^z - e^{-z}}{e^z + e^{-z}} \tag{11}
|
||||
$$
|
||||
|
||||
$$
|
||||
\tanh^{\prime}(z) = 1-y^2 \tag{12}
|
||||
$$
|
||||
|
||||
|
||||
假设某一线性函数 $z_i$ 经过Softmax函数之后的预测输出为 $\hat{y}_ i$,该输出的标签值为 $y_i$,则:
|
||||
|
||||
$$
|
||||
softmax(z_i) = \hat y_i = \frac{e^{z_i}}{\sum_{j=1}^me^{z_j}} \tag{13}
|
||||
$$
|
||||
|
||||
$$
|
||||
\frac{\partial{loss}}{\partial{z_i}} = \hat{y}_ i - y_i \tag{14}
|
||||
$$
|
||||
|
||||
从图中可知,从上层传回的误差为输出层$z_t$向$h^l_t$传回的误差,假设输出层的激活函数为softmax函数,输出层标签值为$y$,则:
|
||||
|
||||
$$
|
||||
\delta^{l+1}_ {x_t} = \frac{\partial{loss}}{\partial{z_t}} \cdot \frac{\partial{z_t}}{\partial{h^l_t}} = (a - y) \cdot V^{\top} \tag{15}
|
||||
$$
|
||||
|
||||
|
||||
从$t+1$时刻传回的误差为$\delta^l_{h_t}$,若$t$为时序的最后一个时间点,则$\delta^l_{h_t}=0$。
|
||||
|
||||
该cell的隐层$h^l_t$的最终误差为两项误差之和,即:
|
||||
|
||||
$$
|
||||
\delta^l_t = \frac{\partial{loss}}{\partial{h_t}} = \delta^l_{h_t} + \delta^{l+1}_{x_t} = (a - y) \cdot V^{\top} \tag{16}
|
||||
$$
|
||||
|
||||
接下来的推导过程仅与本层相关,为了方便推导,我们忽略层次信息,令$\delta^l_t = \delta_t$。
|
||||
|
||||
可以求得各个门结构加权输入的误差,如下:
|
||||
|
||||
$$
|
||||
\begin{aligned}
|
||||
\delta_{z_{ot}} &= \frac{\partial{loss}}{\partial{z_{o_t}}} = \frac{\partial{loss}}{\partial{h_t}} \cdot \frac{\partial{h_t}}{\partial{o_t}} \cdot \frac{\partial{o_t}}{\partial{z_{o_t}}} \\\\
|
||||
&= \delta_t \cdot diag[\tanh(c_t)] \cdot diag[o_t \circ (1 - o_t)] \\\\
|
||||
&= \delta_t \circ \tanh(c_t) \circ o_t \circ (1 - o_t)
|
||||
\end{aligned}
|
||||
\tag{17}
|
||||
$$
|
||||
|
||||
$$
|
||||
\begin{aligned}
|
||||
\delta_{c_t} &= \frac{\partial{loss}}{\partial{c_t}} = \frac{\partial{loss}}{\partial{h_t}} \cdot \frac{\partial{h_t}}{\partial{\tanh(c_t)}} \cdot \frac{\partial{\tanh(c_t)}}{\partial{c_t}} \\\\
|
||||
&= \delta_t \cdot diag[o_t] \cdot diag[1-\tanh^2(c_t)] \\\\
|
||||
&= \delta_t \circ o_t \circ (1-\tanh^2(c_t))
|
||||
\end{aligned}
|
||||
\tag{18}
|
||||
$$
|
||||
|
||||
$$
|
||||
\begin{aligned}
|
||||
\delta_{z_{\tilde{c}t}} &= \frac{\partial{loss}}{\partial{z_{\tilde c_t}}} = \frac{\partial{loss}}{\partial{c_t}} \cdot \frac{\partial{c_t}}{\partial{\tilde c_t}} \cdot \frac{\partial{\tilde c_t}}{\partial{z_{\tilde c_t}}} \\
|
||||
&= \delta_{c_t} \cdot diag[i_t] \cdot diag[1-(\tilde c_t)^2] \\
|
||||
&= \delta_{c_t} \circ i_t \circ (1-(\tilde c_t)^2)
|
||||
\end{aligned}
|
||||
\tag{19}
|
||||
$$
|
||||
|
||||
$$
|
||||
\begin{aligned}
|
||||
\delta_{z_{it}} &= \frac{\partial{loss}}{\partial{z_{i_t}}} = \frac{\partial{loss}}{\partial{c_t}} \cdot \frac{\partial{c_t}}{\partial{i_t}} \cdot \frac{\partial{i_t}}{\partial{z_{i_t}}} \\
|
||||
&= \delta_{c_t} \cdot diag[\tilde c_t] \cdot diag[i_t \circ (1 - i_t)] \\
|
||||
&= \delta_{c_t} \circ \tilde c_t \circ i_t \circ (1 - i_t)
|
||||
\end{aligned}
|
||||
\tag{20}
|
||||
$$
|
||||
|
||||
$$
|
||||
\begin{aligned}
|
||||
\delta_{z_{ft}} &= \frac{\partial{loss}}{\partial{z_{f_t}}} = \frac{\partial{loss}}{\partial{c_t}} \cdot \frac{\partial{c_t}}{\partial{f_t}} \cdot \frac{\partial{f_t}}{\partial{z_{f_t}}} \\\\
|
||||
&= \delta_{c_t} \cdot diag[c_{t-1}] \cdot diag[f_t \circ (1 - f_t)] \\\\
|
||||
&= \delta_{c_t} \circ c_{t-1} \circ f_t \circ (1 - f_t)
|
||||
\end{aligned}
|
||||
\tag{21}
|
||||
$$
|
||||
|
||||
于是,在$t$时刻,输出层参数的各项误差为:
|
||||
|
||||
$$
|
||||
d_{W_{o,t}} = \frac{\partial{loss}}{\partial{W_{o,t}}} = \frac{\partial{loss}}{\partial{z_{o_t}}} \cdot \frac{\partial{z_{o_t}}}{\partial{W_o}} = h^{\top}_ {t-1} \cdot \delta_{z_{ot}}
|
||||
\tag{22}
|
||||
$$
|
||||
|
||||
$$
|
||||
d_{U_{o,t}} = \frac{\partial{loss}}{\partial{U_{o,t}}} = \frac{\partial{loss}}{\partial{z_{o_t}}} \cdot \frac{\partial{z_{o_t}}}{\partial{U_o}} = x^{\top}_ t \cdot \delta_{z_{ot}}
|
||||
\tag{23}
|
||||
$$
|
||||
|
||||
$$
|
||||
d_{b_{o,t}} = \frac{\partial{loss}}{\partial{b_{o,t}}} = \frac{\partial{loss}}{\partial{z_{o_t}}} \cdot \frac{\partial{z_{o_t}}}{\partial{b_o}} = \delta_{z_{ot}}
|
||||
\tag{24}
|
||||
$$
|
||||
|
||||
最终误差为各时刻误差之和,则:
|
||||
|
||||
$$
|
||||
d_{W_o} = \sum^\tau_{t=1}d_{W_{o,t}} = \sum^\tau_{t=1}h^{\top}_ {t-1} \cdot \delta_{z_{ot}}
|
||||
\tag{25}
|
||||
$$
|
||||
|
||||
$$
|
||||
d_{U_o} = \sum^\tau_{t=1}d_{U_{o,t}} = \sum^\tau_{t=1}x^{\top}_ t \cdot \delta_{z_{ot}}
|
||||
\tag{26}
|
||||
$$
|
||||
|
||||
$$
|
||||
d_{b_o} = \sum^\tau_{t=1}d_{b_{o,t}} = \sum^\tau_{t=1}\delta_{z_{ot}}
|
||||
\tag{27}
|
||||
$$
|
||||
|
||||
|
||||
同理可得:
|
||||
|
||||
$$
|
||||
d_{W_{c}} = \sum^\tau_{t=1}d_{W_{c,t}} = \sum^\tau_{t=1}h^{\top}_ {t-1} \cdot \delta_{z_{\tilde{c}t}}
|
||||
\tag{28}
|
||||
$$
|
||||
|
||||
$$
|
||||
d_{U_{c}} = \sum^\tau_{t=1}d_{U_{c,t}} = \sum^\tau_{t=1}x^{\top}_ t \cdot \delta_{z_{\tilde{c}t}}
|
||||
\tag{29}
|
||||
$$
|
||||
|
||||
$$
|
||||
d_{b_{c}} = \sum^\tau_{t=1}d_{b_{c,t}} = \sum^\tau_{t=1}\delta_{z_{\tilde{c}t}}
|
||||
\tag{30}
|
||||
$$
|
||||
|
||||
$$
|
||||
d_{W_{i}} = \sum^\tau_{t=1}d_{W_{i,t}} = \sum^\tau_{t=1}h^{\top}_ {t-1} \cdot \delta_{z_{it}}
|
||||
\tag{31}
|
||||
$$
|
||||
|
||||
$$
|
||||
d_{U_{i}} = \sum^\tau_{t=1}d_{U_{i,t}} = \sum^\tau_{t=1}x^{\top}_ t \cdot \delta_{z_{it}}
|
||||
\tag{32}
|
||||
$$
|
||||
|
||||
$$
|
||||
d_{b_{i}} = \sum^\tau_{t=1}d_{b_{i,t}} =\sum^\tau_{t=1}\delta_{z_{it}}
|
||||
\tag{33}
|
||||
$$
|
||||
|
||||
$$
|
||||
d_{W_{f}} = \sum^\tau_{t=1}d_{W_{f,t}} = \sum^\tau_{t=1}h^{\top}_ {t-1} \cdot \delta_{z_{ft}}
|
||||
\tag{34}
|
||||
$$
|
||||
|
||||
$$
|
||||
d_{U_{f}} = \sum^\tau_{t=1}d_{U_{f,t}} = \sum^\tau_{t=1}x^{\top}_ t \cdot \delta_{z_{ft}}
|
||||
\tag{35}
|
||||
$$
|
||||
|
||||
$$
|
||||
d_{b_{f}} = \sum^\tau_{t=1}d_{b_{f,t}} = \sum^\tau_{t=1}\delta_{z_{ft}}
|
||||
\tag{36}
|
||||
$$
|
||||
|
||||
当前LSTM cell分别向前一时刻($t-1$)和下一层($l-1$)传递误差,公式如下:
|
||||
|
||||
沿时间向前传递:
|
||||
|
||||
$$
|
||||
\begin{aligned}
|
||||
\delta_{h_{t-1}} = \frac{\partial{loss}}{\partial{h_{t-1}}} &= \frac{\partial{loss}}{\partial{z_{ft}}} \cdot \frac{\partial{z_{ft}}}{\partial{h_{t-1}}} + \frac{\partial{loss}}{\partial{z_{it}}} \cdot \frac{\partial{z_{it}}}{\partial{h_{t-1}}} \\\\
|
||||
&+ \frac{\partial{loss}}{\partial{z_{\tilde{c}t}}} \cdot \frac{\partial{z_{\tilde{c}t}}}{\partial{h_{t-1}}} + \frac{\partial{loss}}{\partial{z_{ot}}} \cdot \frac{\partial{z_{ot}}}{\partial{h_{t-1}}} \\\\
|
||||
&= \delta_{z_{ft}} \cdot W_f^{\top} + \delta_{z_{it}} \cdot W_i^{\top} + \delta_{z_{\tilde{c}t}} \cdot W_c^{\top} + \delta_{z_{ot}} \cdot W_o^{\top}
|
||||
\end{aligned}
|
||||
\tag{37}
|
||||
$$
|
||||
|
||||
沿层次向下传递:
|
||||
|
||||
$$
|
||||
\begin{aligned}
|
||||
\delta_{x_t} = \frac{\partial{loss}}{\partial{x_t}} &= \frac{\partial{loss}}{\partial{z_{ft}}} \cdot \frac{\partial{z_{ft}}}{\partial{x_t}} + \frac{\partial{loss}}{\partial{z_{it}}} \cdot \frac{\partial{z_{it}}}{\partial{x_t}} \\\\
|
||||
&+ \frac{\partial{loss}}{\partial{z_{\tilde{c}t}}} \cdot \frac{\partial{z_{\tilde{c}t}}}{\partial{x_t}} + \frac{\partial{loss}}{\partial{z_{ot}}} \cdot \frac{\partial{z_{ot}}}{\partial{x_t}} \\\\
|
||||
&= \delta_{z_{ft}} \cdot U_f^{\top} + \delta_{z_{it}} \cdot U_i^{\top} + \delta_{z_{\tilde{c}t}} \cdot U_c^{\top} + \delta_{z_{ot}} \cdot U_o^{\top}
|
||||
\end{aligned}
|
||||
\tag{38}
|
||||
$$
|
||||
|
||||
以上,LSTM反向传播公式推导完毕。
|
||||
|
||||
|
|
|
@ -3,293 +3,14 @@
|
|||
|
||||
## 20.2 LSTM代码实现
|
||||
|
||||
上一节,我们学习了LSTM的基本原理,本小节我们用代码实现LSTM网络,并用含有4个时序的LSTM进行二进制减法的训练和测试。
|
||||
以下为本小节目录,详情请参阅《智能之门》正版图书,高等教育出版社。
|
||||
|
||||
|
||||
### 20.2.1 LSTM单元的代码实现
|
||||
|
||||
下面是单个LSTM Cell实现的代码。
|
||||
|
||||
#### 初始化
|
||||
|
||||
初始化时需要告知LSTM Cell 输入向量的维度和隐层状态向量的维度,分别为$input\_size$和$hidden\_size$。
|
||||
|
||||
```Python
|
||||
def __init__(self, input_size, hidden_size, bias=True):
|
||||
self.input_size = input_size
|
||||
self.hidden_size = hidden_size
|
||||
self.bias = bias
|
||||
```
|
||||
|
||||
#### 前向计算
|
||||
|
||||
```Python
|
||||
def forward(self, x, h_p, c_p, W, U, b=None):
|
||||
self.get_params(W, U, b)
|
||||
self.x = x
|
||||
|
||||
# caclulate each gate
|
||||
# use g instead of \tilde{c}
|
||||
self.f = self.get_gate(x, h_p, self.wf, self.uf, self.bf, Sigmoid())
|
||||
self.i = self.get_gate(x, h_p, self.wi, self.ui, self.bi, Sigmoid())
|
||||
self.g = self.get_gate(x, h_p, self.wg, self.ug, self.bg, Tanh())
|
||||
self.o = self.get_gate(x, h_p, self.wo, self.uo, self.bo, Sigmoid())
|
||||
# calculate the states
|
||||
self.c = np.multiply(self.f, c_p) + np.multiply(self.i, self.g)
|
||||
self.h = np.multiply(self.o, Tanh().forward(self.c))
|
||||
```
|
||||
|
||||
其中,$get\_params$将传入参数拆分,每个门使用一个独立参数。$get\_gate$实现每个门的前向计算公式。
|
||||
|
||||
```Python
|
||||
def get_params(self, W, U, b=None):
|
||||
self.wf, self.wi, self.wg, self.wo = self.split_params(W, self.hidden_size)
|
||||
self.uf, self.ui, self.ug, self.uo = self.split_params(U, self.input_size)
|
||||
self.bf, self.bi, self.bg, self.bo = self.split_params((b if self.bias else np.zeros((4, self.hidden_size))) , 1)
|
||||
```
|
||||
|
||||
```Python
|
||||
def get_gate(self, x, h, W, U, b, activator):
|
||||
if self.bias:
|
||||
z = np.dot(h, W) + np.dot(x, U) + b
|
||||
else:
|
||||
z = np.dot(h, W) + np.dot(x, U)
|
||||
a = activator.forward(z)
|
||||
return a
|
||||
```
|
||||
|
||||
#### 反向传播
|
||||
|
||||
反向传播过程分为沿时间传播和沿层次传播两部分。$dh$将误差传递给前一个时刻,$dx$将误差传向下一层。
|
||||
|
||||
```Python
|
||||
def backward(self, h_p, c_p, in_grad):
|
||||
tanh = lambda x : Tanh().forward(x)
|
||||
|
||||
self.dzo = in_grad * tanh(self.c) * self.o * (1 - self.o)
|
||||
self.dc = in_grad * self.o * (1 - tanh(self.c) * tanh(self.c))
|
||||
self.dzg = self.dc * self.i * (1- self.g * self.g)
|
||||
self.dzi = self.dc * self.g * self.i * (1 - self.i)
|
||||
self.dzf = self.dc * c_p * self.f * (1 - self.f)
|
||||
|
||||
self.dwo = np.dot(h_p.T, self.dzo)
|
||||
self.dwg = np.dot(h_p.T, self.dzg)
|
||||
self.dwi = np.dot(h_p.T, self.dzi)
|
||||
self.dwf = np.dot(h_p.T, self.dzf)
|
||||
|
||||
self.duo = np.dot(self.x.T, self.dzo)
|
||||
self.dug = np.dot(self.x.T, self.dzg)
|
||||
self.dui = np.dot(self.x.T, self.dzi)
|
||||
self.duf = np.dot(self.x.T, self.dzf)
|
||||
|
||||
if self.bias:
|
||||
self.dbo = np.sum(self.dzo,axis=0, keepdims=True)
|
||||
self.dbg = np.sum(self.dzg,axis=0, keepdims=True)
|
||||
self.dbi = np.sum(self.dzi,axis=0, keepdims=True)
|
||||
self.dbf = np.sum(self.dzf,axis=0, keepdims=True)
|
||||
|
||||
# pass to previous time step
|
||||
self.dh = np.dot(self.dzf, self.wf.T) + np.dot(self.dzi, self.wi.T) + np.dot(self.dzg, self.wg.T) + np.dot(self.dzo, self.wo.T)
|
||||
# pass to previous layer
|
||||
self.dx = np.dot(self.dzf, self.uf.T) + np.dot(self.dzi, self.ui.T) + np.dot(self.dzg, self.ug.T) + np.dot(self.dzo, self.uo.T)
|
||||
```
|
||||
|
||||
最后,我们将所有拆分的参数merge到一起,便于更新梯度。
|
||||
|
||||
```Python
|
||||
def merge_params(self):
|
||||
self.dW = np.concatenate((self.dwf, self.dwi, self.dwg, self.dwo), axis=0)
|
||||
self.dU = np.concatenate((self.duf, self.dui, self.dug, self.duo), axis=0)
|
||||
if self.bias:
|
||||
self.db = np.concatenate((self.dbf, self.dbi, self.dbg, self.dbo), axis=0)
|
||||
```
|
||||
|
||||
以上,完成了LSTM Cell的代码实现。
|
||||
|
||||
通常,LSTM的输出会接一个线性层,得到最终预测输出,即公式$(7)$和$(8)$的内容。
|
||||
|
||||
下面是线性单元的实现代码:
|
||||
|
||||
```Python
|
||||
class LinearCell_1_2(object):
|
||||
def __init__(self, input_size, output_size, activator=None, bias=True):
|
||||
self.input_size = input_size
|
||||
self.output_size = output_size
|
||||
self.bias = bias
|
||||
self.activator = activator
|
||||
|
||||
def forward(self, x, V, b=None):
|
||||
self.x = x
|
||||
self.batch_size = self.x.shape[0]
|
||||
self.V = V
|
||||
self.b = b if self.bias else np.zeros((self.output_size))
|
||||
self.z = np.dot(x, V) + self.b
|
||||
if self.activator:
|
||||
self.a = self.activator.forward(self.z)
|
||||
|
||||
def backward(self, in_grad):
|
||||
self.dz = in_grad
|
||||
self.dV = np.dot(self.x.T, self.dz)
|
||||
if self.bias:
|
||||
# in the sake of backward in batch
|
||||
self.db = np.sum(self.dz, axis=0, keepdims=True)
|
||||
self.dx = np.dot(self.dz, self.V.T)
|
||||
```
|
||||
|
||||
### 20.2.2 用LSTM训练网络
|
||||
|
||||
我们以前面讲过的4位二进制减法为例,验证LSTM网络的正确性。
|
||||
|
||||
该实例需要4个时间步(time steps),我们搭建一个含有4个LSTM单元的单层网络,连接一个线性层,提供最终预测输出。网络结构如图20-5所示.
|
||||
|
||||
<img src="./img/20/lstm_binraryminus_structure.png" />
|
||||
|
||||
图20-5 训练网络结构示意图
|
||||
|
||||
网络初始化,前向计算,反向传播的代码如下:
|
||||
|
||||
```Python
|
||||
class net(object):
|
||||
def __init__(self, dr, input_size, hidden_size, output_size, bias=True):
|
||||
self.dr = dr
|
||||
self.loss_fun = LossFunction_1_1(NetType.BinaryClassifier)
|
||||
self.loss_trace = TrainingHistory_3_0()
|
||||
self.times = 4
|
||||
self.input_size = input_size
|
||||
self.hidden_size = hidden_size
|
||||
self.output_size = output_size
|
||||
self.bias = bias
|
||||
self.lstmcell = []
|
||||
self.linearcell = []
|
||||
#self.a = []
|
||||
for i in range(self.times):
|
||||
self.lstmcell.append(LSTMCell_1_2(input_size, hidden_size, bias=bias))
|
||||
self.linearcell.append(LinearCell_1_2(hidden_size, output_size, Logistic(), bias=bias))
|
||||
#self.a.append((1, self.output_size))
|
||||
|
||||
def forward(self, X):
|
||||
hp = np.zeros((1, self.hidden_size))
|
||||
cp = np.zeros((1, self.hidden_size))
|
||||
for i in range(self.times):
|
||||
self.lstmcell[i].forward(X[:,i], hp, cp, self.W, self.U, self.bh)
|
||||
hp = self.lstmcell[i].h
|
||||
cp = self.lstmcell[i].c
|
||||
self.linearcell[i].forward(hp, self.V, self.b)
|
||||
#self.a[i] = Logistic().forward(self.linearcell[i].z)
|
||||
```
|
||||
|
||||
在反向传播的过程中,不同时间步,误差的来源不同。最后的时间步,传入误差只来自输出层的误差$dx$。其他时间步的误差来自于两个方向$dh$和$dx$(时间和层次)。第一个时间步,传入的状态$h0$,$c0$皆为0。
|
||||
|
||||
```Python
|
||||
def backward(self, Y):
|
||||
hp = []
|
||||
cp = []
|
||||
# The last time step:
|
||||
tl = self.times-1
|
||||
dz = self.linearcell[tl].a - Y[:,tl:tl+1]
|
||||
self.linearcell[tl].backward(dz)
|
||||
hp = self.lstmcell[tl-1].h
|
||||
cp = self.lstmcell[tl-1].c
|
||||
self.lstmcell[tl].backward(hp, cp, self.linearcell[tl].dx)
|
||||
# Middle time steps:
|
||||
dh = []
|
||||
for i in range(tl-1, 0, -1):
|
||||
dz = self.linearcell[i].a - Y[:,i:i+1]
|
||||
self.linearcell[i].backward(dz)
|
||||
hp = self.lstmcell[i-1].h
|
||||
cp = self.lstmcell[i-1].c
|
||||
dh = self.linearcell[i].dx + self.lstmcell[i+1].dh
|
||||
self.lstmcell[i].backward(hp, cp, dh)
|
||||
# The first time step:
|
||||
dz = self.linearcell[0].a - Y[:,0:1]
|
||||
self.linearcell[0].backward(dz)
|
||||
dh = self.linearcell[0].dx + self.lstmcell[1].dh
|
||||
self.lstmcell[0].backward(np.zeros((self.batch_size, self.hidden_size)), np.zeros((self.batch_size, self.hidden_size)), dh)
|
||||
```
|
||||
|
||||
下面就可以开始训练了,训练部分主要分为:初始化参数,训练网络,更新参数,计算误差几个部分。主要代码如下:
|
||||
|
||||
```Python
|
||||
def train(self, batch_size, checkpoint=0.1):
|
||||
self.batch_size = batch_size
|
||||
max_epoch = 100
|
||||
eta = 0.1
|
||||
# Try different initialize method
|
||||
#self.U = np.random.random((4 * self.input_size, self.hidden_size))
|
||||
#self.W = np.random.random((4 * self.hidden_size, self.hidden_size))
|
||||
self.U = self.init_params_uniform((4 * self.input_size, self.hidden_size))
|
||||
self.W = self.init_params_uniform((4 * self.hidden_size, self.hidden_size))
|
||||
self.V = np.random.random((self.hidden_size, self.output_size))
|
||||
self.bh = np.zeros((4, self.hidden_size))
|
||||
self.b = np.zeros((self.output_size))
|
||||
|
||||
max_iteration = math.ceil(self.dr.num_train/batch_size)
|
||||
checkpoint_iteration = (int)(math.ceil(max_iteration * checkpoint))
|
||||
|
||||
for epoch in range(max_epoch):
|
||||
self.dr.Shuffle()
|
||||
for iteration in range(max_iteration):
|
||||
# get data
|
||||
batch_x, batch_y = self.dr.GetBatchTrainSamples(batch_size, iteration)
|
||||
# forward
|
||||
self.forward(batch_x)
|
||||
self.backward(batch_y)
|
||||
# update
|
||||
for i in range(self.times):
|
||||
self.lstmcell[i].merge_params()
|
||||
self.U = self.U - self.lstmcell[i].dU * eta /self.batch_size
|
||||
self.W = self.W - self.lstmcell[i].dW * eta /self.batch_size
|
||||
self.V = self.V - self.linearcell[i].dV * eta /self.batch_size
|
||||
if self.bias:
|
||||
self.bh = self.bh - self.lstmcell[i].db * eta /self.batch_size
|
||||
self.b = self.b - self.linearcell[i].db * eta /self.batch_size
|
||||
# check loss
|
||||
total_iteration = epoch * max_iteration + iteration
|
||||
if (total_iteration+1) % checkpoint_iteration == 0:
|
||||
X,Y = self.dr.GetValidationSet()
|
||||
loss,acc,_ = self.check_loss(X,Y)
|
||||
self.loss_trace.Add(epoch, total_iteration, None, None, loss, acc, None)
|
||||
print(epoch, total_iteration)
|
||||
print(str.format("loss={0:6f}, acc={1:6f}", loss, acc))
|
||||
#end if
|
||||
#enf for
|
||||
if (acc == 1.0):
|
||||
break
|
||||
#end for
|
||||
self.loss_trace.ShowLossHistory("Loss and Accuracy", XCoordinate.Iteration)
|
||||
```
|
||||
|
||||
### 20.2.3 最终结果
|
||||
|
||||
图20-6展示了训练过程,以及loss和accuracy的曲线变化。
|
||||
|
||||
<img src="./img/20/loss_acc_binaryminus_using_lstm.png">
|
||||
|
||||
图20-6 loss和accuracy的曲线变化图
|
||||
|
||||
该模型在验证集上可得100%的正确率。随机测试样例预测值与真实值完全一致。网络正确性得到验证。
|
||||
|
||||
```
|
||||
x1: [1, 1, 0, 1]
|
||||
- x2: [1, 0, 0, 1]
|
||||
------------------
|
||||
true: [0, 1, 0, 0]
|
||||
pred: [0, 1, 0, 0]
|
||||
13 - 9 = 4
|
||||
====================
|
||||
x1: [1, 0, 0, 0]
|
||||
- x2: [0, 1, 0, 1]
|
||||
------------------
|
||||
true: [0, 0, 1, 1]
|
||||
pred: [0, 0, 1, 1]
|
||||
8 - 5 = 3
|
||||
====================
|
||||
x1: [1, 1, 0, 0]
|
||||
- x2: [1, 0, 0, 1]
|
||||
------------------
|
||||
true: [0, 0, 1, 1]
|
||||
pred: [0, 0, 1, 1]
|
||||
12 - 9 = 3
|
||||
```
|
||||
|
||||
### 代码位置
|
||||
|
||||
|
|
|
@ -3,319 +3,20 @@
|
|||
|
||||
## 20.3 GRU基本原理与实现
|
||||
|
||||
以下为本小节目录,详情请参阅《智能之门》正版图书,高等教育出版社。
|
||||
|
||||
### 20.3.1 GRU 的基本概念
|
||||
|
||||
LSTM 存在很多变体,其中门控循环单元(Gated Recurrent Unit, GRU)是最常见的一种,也是目前比较流行的一种。GRU是由 [Cho](https://arxiv.org/pdf/1406.1078v3.pdf) 等人在2014年提出的,它对LSTM做了一些简化:
|
||||
|
||||
1. GRU将LSTM原来的三个门简化成为两个:重置门 $r_t$(Reset Gate)和更新门 $z_t$ (Update Gate)。
|
||||
2. GRU不保留单元状态 $c_t$,只保留隐藏状态 $h_t$作为单元输出,这样就和传统RNN的结构保持一致。
|
||||
3. 重置门直接作用于前一时刻的隐藏状态 $h_{t-1}$。
|
||||
|
||||
|
||||
### 20.3.2 GRU的前向计算
|
||||
|
||||
#### GRU的单元结构
|
||||
|
||||
图20-7展示了GRU的单元结构。
|
||||
|
||||
<img src="./img/20/gru_structure.png" />
|
||||
|
||||
图20-7 GRU单元结构图
|
||||
|
||||
GRU单元的前向计算公式如下:
|
||||
|
||||
1. 更新门
|
||||
|
||||
$$
|
||||
z_t = \sigma(h_{t-1} \cdot W_z + x_t \cdot U_z)
|
||||
\tag{1}
|
||||
$$
|
||||
|
||||
2. 重置门
|
||||
|
||||
$$
|
||||
r_t = \sigma(h_{t-1} \cdot W_r + x_t \cdot U_r)
|
||||
\tag{2}
|
||||
$$
|
||||
|
||||
3. 候选隐藏状态
|
||||
|
||||
$$
|
||||
\tilde h_t = \tanh((r_t \circ h_{t-1}) \cdot W_h + x_t \cdot U_h)
|
||||
\tag{3}
|
||||
$$
|
||||
|
||||
4. 隐藏状态
|
||||
|
||||
$$
|
||||
h = (1 - z_t) \circ h_{t-1} + z_t \circ \tilde{h}_t
|
||||
\tag{4}
|
||||
$$
|
||||
|
||||
#### GRU的原理浅析
|
||||
|
||||
从上面的公式可以看出,GRU通过更新们和重置门控制长期状态的遗忘和保留,以及当前输入信息的选择。更新门和重置门通过$sigmoid$函数,将输入信息映射到$[0,1]$区间,实现门控功能。
|
||||
|
||||
首先,上一时刻的状态$h_{t-1}$通过重置门,加上当前时刻输入信息,共同构成当前时刻的即时状态$\tilde{h}_t$,并通过$\tanh$函数映射到$[-1,1]$区间。
|
||||
|
||||
然后,通过更新门实现遗忘和记忆两个部分。从隐藏状态的公式可以看出,通过$z_t$进行选择性的遗忘和记忆。$(1-z_t)$和$z_t$有联动关系,上一时刻信息遗忘的越多,当前信息记住的就越多,实现了LSTM中$f_t$和$i_t$的功能。
|
||||
|
||||
### 20.3.3 GRU的反向传播
|
||||
|
||||
学习了LSTM的反向传播的推导,GRU的推导就相对简单了。我们仍然以$l$层$t$时刻的GRU单元为例,推导反向传播过程。
|
||||
|
||||
同LSTM, 令:$l$层$t$时刻传入误差为$\delta_{t}^l$,为下一时刻传入误差$\delta_{h_t}^l$和上一层传入误差$\delta_{x_t}^{l+1}$之和,简写为$\delta_{t}$。
|
||||
|
||||
令:
|
||||
|
||||
$$
|
||||
z_{zt} = h_{t-1} \cdot W_z + x_t \cdot U_z
|
||||
\tag{5}
|
||||
$$
|
||||
|
||||
$$
|
||||
z_{rt} = h_{t-1} \cdot W_r + x_t \cdot U_r
|
||||
\tag{6}
|
||||
$$
|
||||
|
||||
$$
|
||||
z_{\tilde h_t} = (r_t \circ h_{t-1}) \cdot W_h + x_t \cdot U_h
|
||||
\tag{7}
|
||||
$$
|
||||
|
||||
则:
|
||||
|
||||
$$
|
||||
\begin{aligned}
|
||||
\delta_{z_{zt}} &= \frac{\partial{loss}}{\partial{h_t}} \cdot \frac{\partial{h_t}}{\partial{z_t}} \cdot \frac{\partial{z_t}}{\partial{z_{z_t}}} \\\\
|
||||
&= \delta_t \cdot (-diag[h_{t-1}] + diag[\tilde h_t]) \cdot diag[z_t \circ (1-z_t)] \\\\
|
||||
&= \delta_t \circ (\tilde h_t - h_{t-1}) \circ z_t \circ (1-z_t)
|
||||
\end{aligned}
|
||||
\tag{8}
|
||||
$$
|
||||
|
||||
$$
|
||||
\begin{aligned}
|
||||
\delta_{z_{\tilde{h}t}} &= \frac{\partial{loss}}{\partial{h_t}} \cdot \frac{\partial{h_t}}{\partial{\tilde h_t}} \cdot \frac{\partial{\tilde h_t}}{\partial{z_{\tilde h_t}}} \\\\
|
||||
&= \delta_t \cdot diag[z_t] \cdot diag[1-(\tilde h_t)^2] \\\\
|
||||
&= \delta_t \circ z_t \circ (1-(\tilde h_t)^2)
|
||||
\end{aligned}
|
||||
\tag{9}
|
||||
$$
|
||||
|
||||
$$
|
||||
\begin{aligned}
|
||||
\delta_{z_{rt}} &= \frac{\partial{loss}}{\partial{\tilde h_t}} \cdot \frac{\partial{\tilde h_t}}{\partial{z_{\tilde h_t}}} \cdot \frac{\partial{z_{\tilde h_t}}}{\partial{r_t}} \cdot \frac{\partial{r_t}}{\partial{z_{r_t}}} \\\\
|
||||
&= \delta_{z_{\tilde{h}t}} \cdot W_h^T \cdot diag[h_{t-1}] \cdot diag[r_t \circ (1-r_t)] \\\\
|
||||
&= \delta_{z_{\tilde{h}t}} \cdot W_h^T \circ h_{t-1} \circ r_t \circ (1-r_t)
|
||||
\end{aligned}
|
||||
\tag{10}
|
||||
$$
|
||||
|
||||
由此可求出,$t$时刻各个可学习参数的误差:
|
||||
|
||||
$$
|
||||
\begin{aligned}
|
||||
d_{W_{h,t}} = \frac{\partial{loss}}{\partial{z_{\tilde h_t}}} \cdot \frac{\partial{z_{\tilde h_t}}}{\partial{W_h}} = (r_t \circ h_{t-1})^{\top} \cdot \delta_{z_{\tilde{h}t}}
|
||||
\end{aligned}
|
||||
\tag{11}
|
||||
$$
|
||||
|
||||
$$
|
||||
\begin{aligned}
|
||||
d_{U_{h,t}} = \frac{\partial{loss}}{\partial{z_{\tilde h_t}}} \cdot \frac{\partial{z_{\tilde h_t}}}{\partial{U_h}} = x_t^{\top} \cdot \delta_{z_{\tilde{h}t}}
|
||||
\end{aligned}
|
||||
\tag{12}
|
||||
$$
|
||||
|
||||
$$
|
||||
\begin{aligned}
|
||||
d_{W_{r,t}} = \frac{\partial{loss}}{\partial{z_{r_t}}} \cdot \frac{\partial{z_{r_t}}}{\partial{W_r}} = h_{t-1}^{\top} \cdot \delta_{z_{rt}}
|
||||
\end{aligned}
|
||||
\tag{13}
|
||||
$$
|
||||
|
||||
$$
|
||||
\begin{aligned}
|
||||
d_{U_{r,t}} = \frac{\partial{loss}}{\partial{z_{r_t}}} \cdot \frac{\partial{z_{r_t}}}{\partial{U_r}} = x_t^{\top} \cdot \delta_{z_{rt}}
|
||||
\end{aligned}
|
||||
\tag{14}
|
||||
$$
|
||||
|
||||
$$
|
||||
\begin{aligned}
|
||||
d_{W_{z,t}} = \frac{\partial{loss}}{\partial{z_{z_t}}} \cdot \frac{\partial{z_{z_t}}}{\partial{W_z}} = h_{t-1}^{\top} \cdot \delta_{z_{zt}}
|
||||
\end{aligned}
|
||||
\tag{15}
|
||||
$$
|
||||
|
||||
$$
|
||||
\begin{aligned}
|
||||
d_{U_{z,t}} = \frac{\partial{loss}}{\partial{z_{z_t}}} \cdot \frac{\partial{z_{z_t}}}{\partial{U_z}} = x_t^{\top} \cdot \delta_{z_{zt}}
|
||||
\end{aligned}
|
||||
\tag{16}
|
||||
$$
|
||||
|
||||
可学习参数的最终误差为各个时刻误差之和,即:
|
||||
|
||||
$$
|
||||
d_{W_h} = \sum_{t=1}^{\tau} d_{W_{h,t}} = \sum_{t=1}^{\tau} (r_t \circ h_{t-1})^{\top} \cdot \delta_{z_{\tilde{h}t}}
|
||||
\tag{17}
|
||||
$$
|
||||
|
||||
$$
|
||||
d_{U_h} = \sum_{t=1}^{\tau} d_{U_{h,t}} = \sum_{t=1}^{\tau} x_t^{\top} \cdot \delta_{z_{\tilde{h}t}}
|
||||
\tag{18}
|
||||
$$
|
||||
|
||||
$$
|
||||
d_{W_r} = \sum_{t=1}^{\tau} d_{W_{r,t}} = \sum_{t=1}^{\tau} h_{t-1}^{\top} \cdot \delta_{z_{rt}}
|
||||
\tag{19}
|
||||
$$
|
||||
|
||||
$$
|
||||
d_{U_r} = \sum_{t=1}^{\tau} d_{U_{r,t}} = \sum_{t=1}^{\tau} x_t^{\top} \cdot \delta_{z_{rt}}
|
||||
\tag{20}
|
||||
$$
|
||||
|
||||
$$
|
||||
d_{W_z} = \sum_{t=1}^{\tau} d_{W_{z,t}} = \sum_{t=1}^{\tau} h_{t-1}^{\top} \cdot \delta_{z_{zt}}
|
||||
\tag{21}
|
||||
$$
|
||||
|
||||
$$
|
||||
d_{U_z} = \sum_{t=1}^{\tau} d_{U_{z,t}} = \sum_{t=1}^{\tau} x_t^{\top} \cdot \delta_{z_{zt}}
|
||||
\tag{22}
|
||||
$$
|
||||
|
||||
当前GRU cell分别向前一时刻($t-1$)和下一层($l-1$)传递误差,公式如下:
|
||||
|
||||
沿时间向前传递:
|
||||
|
||||
$$
|
||||
\begin{aligned}
|
||||
\delta_{h_{t-1}} = \frac{\partial{loss}}{\partial{h_{t-1}}} &= \frac{\partial{loss}}{\partial{h_t}} \cdot \frac{\partial{h_t}}{\partial{h_{t-1}}} + \frac{\partial{loss}}{\partial{z_{\tilde h_t}}} \cdot \frac{\partial{z_{\tilde h_t}}}{\partial{h_{t-1}}} \\\\
|
||||
&+ \frac{\partial{loss}}{\partial{z_{rt}}} \cdot \frac{\partial{z_{rt}}}{\partial{h_{t-1}}} + \frac{\partial{loss}}{\partial{z_{zt}}} \cdot \frac{\partial{z_{zt}}}{\partial{h_{t-1}}} \\\\
|
||||
&= \delta_{t} \circ (1-z_t) + \delta_{z_{\tilde{h}t}} \cdot W_h^{\top} \circ r_t \\\\
|
||||
&+ \delta_{z_{rt}} \cdot W_r^{\top} + \delta_{z_{zt}} \cdot W_z^{\top}
|
||||
\end{aligned}
|
||||
\tag{23}
|
||||
$$
|
||||
|
||||
沿层次向下传递:
|
||||
|
||||
$$
|
||||
\begin{aligned}
|
||||
\delta_{x_t} &= \frac{\partial{loss}}{\partial{x_t}} = \frac{\partial{loss}}{\partial{z_{\tilde h_t}}} \cdot \frac{\partial{z_{\tilde h_t}}}{\partial{x_t}} \\\\
|
||||
&+ \frac{\partial{loss}}{\partial{z_{r_t}}} \cdot \frac{\partial{z_{r_t}}}{\partial{x_t}} + \frac{\partial{loss}}{\partial{z_{z_t}}} \cdot \frac{\partial{z_{z_t}}}{\partial{x_t}} \\\\
|
||||
&= \delta_{z_{\tilde{h}t}} \cdot U_h^{\top} + \delta_{z_{rt}} \cdot U_r^{\top} + \delta_{z_{zt}} \cdot U_z^{\top}
|
||||
\end{aligned}
|
||||
\tag{24}
|
||||
$$
|
||||
|
||||
以上,GRU反向传播公式推导完毕。
|
||||
|
||||
### 20.3.4 代码实现
|
||||
|
||||
本节进行了GRU网络单元前向计算和反向传播的实现。为了统一和简单,测试用例依然是二进制减法。
|
||||
|
||||
#### 初始化
|
||||
|
||||
本案例实现了没有bias的GRU单元,只需初始化输入维度和隐层维度。
|
||||
|
||||
```Python
|
||||
def __init__(self, input_size, hidden_size):
|
||||
self.input_size = input_size
|
||||
self.hidden_size = hidden_size
|
||||
```
|
||||
|
||||
#### 前向计算
|
||||
|
||||
```Python
|
||||
def forward(self, x, h_p, W, U):
|
||||
self.get_params(W, U)
|
||||
self.x = x
|
||||
|
||||
self.z = Sigmoid().forward(np.dot(h_p, self.wz) + np.dot(x, self.uz))
|
||||
self.r = Sigmoid().forward(np.dot(h_p, self.wr) + np.dot(x, self.ur))
|
||||
self.n = Tanh().forward(np.dot((self.r * h_p), self.wn) + np.dot(x, self.un))
|
||||
self.h = (1 - self.z) * h_p + self.z * self.n
|
||||
|
||||
def split_params(self, w, size):
|
||||
s=[]
|
||||
for i in range(3):
|
||||
s.append(w[(i*size):((i+1)*size)])
|
||||
return s[0], s[1], s[2]
|
||||
|
||||
# Get shared parameters, and split them to fit 3 gates, in the order of z, r, \tilde{h} (n stands for \tilde{h} in code)
|
||||
def get_params(self, W, U):
|
||||
self.wz, self.wr, self.wn = self.split_params(W, self.hidden_size)
|
||||
self.uz, self.ur, self.un = self.split_params(U, self.input_size)
|
||||
```
|
||||
|
||||
#### 反向传播
|
||||
|
||||
```Python
|
||||
def backward(self, h_p, in_grad):
|
||||
self.dzz = in_grad * (self.n - h_p) * self.z * (1 - self.z)
|
||||
self.dzn = in_grad * self.z * (1 - self.n * self.n)
|
||||
self.dzr = np.dot(self.dzn, self.wn.T) * h_p * self.r * (1 - self.r)
|
||||
|
||||
self.dwn = np.dot((self.r * h_p).T, self.dzn)
|
||||
self.dun = np.dot(self.x.T, self.dzn)
|
||||
self.dwr = np.dot(h_p.T, self.dzr)
|
||||
self.dur = np.dot(self.x.T, self.dzr)
|
||||
self.dwz = np.dot(h_p.T, self.dzz)
|
||||
self.duz = np.dot(self.x.T, self.dzz)
|
||||
|
||||
self.merge_params()
|
||||
|
||||
# pass to previous time step
|
||||
self.dh = in_grad * (1 - self.z) + np.dot(self.dzn, self.wn.T) * self.r + np.dot(self.dzr, self.wr.T) + np.dot(self.dzz, self.wz.T)
|
||||
# pass to previous layer
|
||||
self.dx = np.dot(self.dzn, self.un.T) + np.dot(self.dzr, self.ur.T) + np.dot(self.dzz, self.uz.T)
|
||||
```
|
||||
|
||||
我们将所有拆分的参数merge到一起,便于更新梯度。
|
||||
|
||||
```Python
|
||||
def merge_params(self):
|
||||
self.dW = np.concatenate((self.dwz, self.dwr, self.dwn), axis=0)
|
||||
self.dU = np.concatenate((self.duz, self.dur, self.dun), axis=0)
|
||||
|
||||
```
|
||||
### 20.3.5 最终结果
|
||||
|
||||
图20-8展示了训练过程,以及loss和accuracy的曲线变化。
|
||||
|
||||
<img src="./img/20/loss_acc_binaryminus_using_gru.png" />
|
||||
|
||||
图20-8 loss和accuracy的曲线变化图
|
||||
|
||||
该模型在验证集上可得100%的正确率。网络正确性得到验证。
|
||||
|
||||
```
|
||||
x1: [1, 1, 1, 0]
|
||||
- x2: [1, 0, 0, 0]
|
||||
------------------
|
||||
true: [0, 1, 1, 0]
|
||||
pred: [0, 1, 1, 0]
|
||||
14 - 8 = 6
|
||||
====================
|
||||
x1: [1, 1, 0, 0]
|
||||
- x2: [0, 0, 0, 0]
|
||||
------------------
|
||||
true: [1, 1, 0, 0]
|
||||
pred: [1, 1, 0, 0]
|
||||
12 - 0 = 12
|
||||
====================
|
||||
x1: [1, 0, 1, 0]
|
||||
- x2: [0, 0, 0, 1]
|
||||
------------------
|
||||
true: [1, 0, 0, 1]
|
||||
pred: [1, 0, 0, 1]
|
||||
10 - 1 = 9
|
||||
```
|
||||
|
||||
### 代码位置
|
||||
|
||||
|
|
|
@ -3,85 +3,10 @@
|
|||
|
||||
## 20.4 序列到序列模型
|
||||
|
||||
序列到序列模型在自然语言处理中应用广泛,是重要的模型结构。本小节对序列到序列模型的提出和结构进行简要介绍,没有涉及代码实现部分。
|
||||
以下为本小节目录,详情请参阅《智能之门》正版图书,高等教育出版社。
|
||||
|
||||
### 20.4.1 提出问题
|
||||
|
||||
前面章节讲到的RNN模型和实例,都属于序列预测问题,或是通过序列中一个时间步的输入值,预测下一个时间步输出值(如二进制减法问题);或是对所有输入序列得到一个输出作为分类(如名字分类问题)。他们的共同特点是:输出序列与输入序列等长,或输出长度为1。
|
||||
|
||||
还有一类序列预测问题,以序列作为输入,需要输出也是序列,并且输入和输出序列长度不确定,并不断变化。这类问题被成为序列到序列(Sequence-to-Sequence, Seq2Seq)预测问题。
|
||||
|
||||
序列到序列问题有很多应用场景:比如机器翻译、问答系统(QA)、文档摘要生成等。简单的RNN或LSRM结构无法处理这类问题,于是科学家们提出了一种新的结构 —— 编码解码(Encoder-Decoder)结构。
|
||||
|
||||
|
||||
### 20.4.2 编码-解码结构(Encoder-Decoder)
|
||||
|
||||
图20-9 为Encoder-Decoder结构的示意图。
|
||||
|
||||
<img src="./img/20/encoder-decoder.png" />
|
||||
|
||||
图20-9 Encoder-Decoder结构示意图
|
||||
|
||||
Encoder-Decoder结构的处理流程非常简单直观。
|
||||
|
||||
- 示意图中,输入序列和输出序列分别为中文语句和翻译之后的英文语句,它们的长度不一定相同。通常会将输入序列嵌入(Embedding)成一定维度的向量,传入编码器。
|
||||
- Encoder为编码器,将输入序列编码成为固定长度的状态向量,通常称为语义编码向量。
|
||||
- Decoder为解码器,将语义编码向量作为原始输入,解码成所需要的输出序列。
|
||||
|
||||
在具体实现中,编码器、解码器可以有不同选择,可自由组合。常见的选择有CNN、RNN、GRU、LSTM等。
|
||||
|
||||
应用Encoder-Decoder结构,可构建出序列到序列模型。
|
||||
|
||||
|
||||
### 20.4.3 序列到序列模型(Seq2Seq)
|
||||
|
||||
Seq2Seq模型有两种常见结构。我们以RNN网络作为编码和解码器来进行讲解。
|
||||
|
||||
图20-10和图20-11分别展示了这两种结构。
|
||||
|
||||
<img src="./img/20/Seq2Seq_structure1.png" />
|
||||
|
||||
图20-10 Seq2Seq结构一
|
||||
|
||||
<img src="./img/20/Seq2Seq_structure2.png" />
|
||||
|
||||
图20-11 Seq2Seq结构二
|
||||
|
||||
#### 编码过程
|
||||
|
||||
两种结构的编码过程完全一致。
|
||||
|
||||
输入序列为 $x=[x1, x2, x3]$。
|
||||
|
||||
RNN网络中,每个时间节点隐层状态为:
|
||||
|
||||
$$
|
||||
h_t = f(h_{t-1}, x_t), \quad t \in [1,3]
|
||||
$$
|
||||
|
||||
编码器中输出的语义编码向量可以有三种不同选取方式,分别是:
|
||||
|
||||
$$
|
||||
\begin{aligned}
|
||||
c &= h_3 \\\\
|
||||
c &= g(h_3) \\\\
|
||||
c &= g(h1, h2, h3) \\\\
|
||||
\end{aligned}
|
||||
$$
|
||||
|
||||
#### 解码过程
|
||||
|
||||
两种结构解码过程的不同点在于,语义编码向量是否应用于每一时刻输入。
|
||||
|
||||
第一种结构,每一时刻的输出$y_t$由前一时刻的输出$y_{t-1}$、前一时刻的隐层状态$h^\prime_{t-1}$和$c$共同决定,即: $y_t = f(y_{t-1}, h^\prime_{t-1}, c)$。
|
||||
|
||||
第二种结构,$c$只作为初始状态传入解码器,并不参与每一时刻的输入,即:
|
||||
|
||||
$$
|
||||
\begin{cases}
|
||||
y_1 = f(y_0, h^\prime_{0}, c) \\\\
|
||||
y_t = f(y_{t-1}, h^\prime_{t-1}), t \in [2,4]
|
||||
\end{cases}
|
||||
$$
|
||||
|
||||
以上是序列到序列模型的结构简介,具体实现将在以后补充。
|
||||
|
|
Загрузка…
Ссылка в новой задаче