* del ch9

* del 10,11,12

* del 7 step

* del step 8

* del step 9
This commit is contained in:
xiaowuhu 2023-10-16 14:23:17 +08:00 коммит произвёл GitHub
Родитель eea1c6fd01
Коммит e8f6f6c039
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
60 изменённых файлов: 95 добавлений и 11591 удалений

Просмотреть файл

@ -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.2Sigmoid函数值是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非常相像的激活函数他们的性质也确实如此。但是比起SigmoidTanh减少了一个缺点就是他本身是零均值的也就是说在传递过程中输入数据的均值并不会发生改变这就使他在很多应用中能表现出比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.5x^2=0.25x^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的尺寸是3x1B2的尺寸是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 Bengio2012是一个替代网格搜索的方法并且编程简单使用更方便能更快地收敛到超参数的良好取值。
随机搜索过程如下:
首先,我们为每个超参 数定义一个边缘分布例如Bernoulli分布或范畴分布分别对应着二元超参数或离散超参数或者对数尺度上的均匀分布对应着正实 值超参数)。例如,其中,$U(a,b)$ 表示区间$(a,b)$ 上均匀采样的样本。类似地,`log_number_of_hidden_units`可以从 $U(\ln(50),\ln(2000))$ 上采样。
与网格搜索不同,我们不需要离散化超参数的值。这允许我们在一个更大的集合上进行搜索,而不产生额外的计算代价。实际上,当有几个超参数对性能度量没有显著影响时,随机搜索相比于网格搜索指数级地高效。
Bergstra and Bengio2012进行了详细的研究并发现相比于网格搜索随机搜索能够更快地减小验证集误差就每个模型运行的试验数而 言)。
与网格搜索一样,我们通常会重复运行不同 版本的随机搜索,以基于前一次运行的结果改进下一次搜索。
随机搜索能比网格搜索更快地找到良好超参数的原因是,没有浪费的实验,不像网格搜索有时会对一个超参数的两个不同值(给定其他超参 数值不变)给出相同结果。在网格搜索中,其他超参数将在这两次实验中拥有相同的值,而在随机搜索中,它们通常会具有不同的值。因此,如果这两个值的变化所对应的验证集误差没有明显区别的话,网格搜索没有必要重复两个等价的实验,而随机搜索仍然会对其他超参数进行两次独立的探索。
贝叶斯优化是另外一种比较成熟技术,有兴趣的读者请自行学习。
### 思考与练习

Просмотреть файл

@ -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章中的逻辑与门和或门一样我们需要神经网络的运算结果达到一定的精度也就是非常的接近01两端而不是说勉强大于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为x1A2为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和x2Z值是原始的标签值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或更多时基本无法理解空间变换的样子了。但是有一个方法可以近似地解释高维情况在三维空间时蓝色点会被推挤到一个角落形成一个三角形那么在NN>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倍那么在随机梯度下降SGDstochastic 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批大小128Xavier初始化最大训练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.60.70.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.20.30.50.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_living152015年翻修后的居住面积
- sqft_lot152015年翻修后的停车场面积
一些考虑:
- 唯一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变成0160也变成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.128470.36453),而第二次初始化得到(0.233340.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 DistributionU表示均匀分布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 则不然对不同的训练迭代次数tAdaGrad 对每个参数都有一个不同的学习率。这里开方、除法和乘法的运算都是按元素运算的。这些按元素运算使得目标函数自变量中每个元素都分别拥有自己的学习率。
#### 输入和参数
- $\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形式一般取3264等数值。
具体的数据处理过程如图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$
从公式89
$$
\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的右侧第五部分从公式78
$$
\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个epoch5763个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 TheoremNFL是由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值不为0xy值为0左右的两个顶点是x值不为0yz值为0前后的两个顶点是y值不为0xz值为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
迭代次数加1i++
计算验证集损失函数值newLoss = loss
if (newLoss < lastLoss) // 新的损失值更小
忍耐次数计数器归零counter = 0
记录当前最佳权重矩阵训练参数theta_best = theta
记录当前迭代次数i_best = i
更新最新验证集损失函数值lastLoss = newLoss
else // 新的损失值大于上一步的损失值
忍耐次数计数器加1counter++
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]
$$
或者其它一些分布。
从公式3Y将会是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-1Keanrl-2Kernal-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对应b1Filter-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=5KernalWidth=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时需要补一个双线的十字。所以当知道当前的卷积层步长为SS>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 偏移的梯度计算
根据前向计算公式1234可以得到
$$
\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=2x2stride=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的是一条线标记为UU是一个标量参数在图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所示二者有些许不同但是还是可以从中总结出关于梯度计算和存储的一般性的规律即通过时间的反向传播BPTTBack 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变量在第一个时间步是Nonenext_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-120-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}
$$
以上是序列到序列模型的结构简介,具体实现将在以后补充。