# 《深度学习入门》的cpp实现-ch06: 与学习相关的技巧

上一篇文章中，我们使用随机正态分布初始化神经网络的权重，SGD的方法更新参数，对于学习率、batch大小、权重衰减率等超参数选择了固定值，这篇文章我们会探讨对于这些做法的改进方案。

由于这一节涉及到众多方法， 并且要求网络结构复杂，不能逐一进行**"cpp实现"**，理解原理并掌握即可(所以请把这篇文章当作读书笔记看待)。

## 参数的更新

### SGD
SGD公式如下
$$W \gets W - \eta \frac{\partial	L}{\partial W}	$$

SGD原理简单，并且在代码上容易实现，但是针对某些参数空间， 解决问题的效率比较低下，比方说对于
$$f(x,y) = \frac{1}{20}x^2 + y^2$$


![image.png](https://cdn.hashnode.com/res/hashnode/image/upload/v1661315145851/agZA1u1AR.png align="left")

梯度特征是，在y轴方向上很大，在x轴方向上很小。虽然在`(x,y)`=`(0,0)`处梯度最小，但是很多地方梯度并没有指向`(0, 0)`。

对于这种函数形状**非均向(anisotropic)**，比如呈延伸状，搜索的路径就会非常低效，根本原因是梯度的方向并没有指向最小值的方向。


![image.png](https://cdn.hashnode.com/res/hashnode/image/upload/v1661323349749/q3Erv8xqw.png align="left")

### Momentum
Momentum是“动量”的意思，公式如下
$$v \gets \alpha v - \eta \frac{\partial	L}{\partial W}$$
$$W \gets W + v$$
`η` 表示学习率; `v`对应物理上的速度; `αv` 在物体不受任何力时，该项承担使物体逐渐减速的任务,对应物理上的地面摩擦或空气阻力。

在 Momentum 公式中，`W` 保存了过往梯度的矢量和。如果在一个方向上，梯度方向变化反复，那么经过多次，在一个方向上的力会被抵消；如果在一个方向上，梯度很小，但是持续不变，那么经过多次，在这个方向上的力就会有一个“加速度”。


![image.png](https://cdn.hashnode.com/res/hashnode/image/upload/v1661325358887/O5AYvNnCT.png align="left")

> 和 SGD 相比，我们发现“之”字形的“程度”减轻了。这是因为虽然 x 轴方向上受到的力非常小，但是一直在同一方向上受力，所以朝同一个方向会有一定的加速。反过来，虽然 y 轴方向上受到的力很大，但是因为交互地受到正方向和反方向的力，它
们会互相抵消，所以 y 轴方向上的速度不稳定。

### AdaGrad
在神经网络的学习中，学习率过小， 会导致学习花费过多时间；学习率过大，则会导致学习发散而不能正确进行。

AdaGrad 会为参数的每个元素适当地调整学习率，与此同时进行学习 (AdaGrad 的 Ada 来自英文单词 Adaptive，即“适当的”的意思)。

$$h \gets h +  \frac{\partial	L}{\partial W} \odot  \frac{\partial	L}{\partial W} $$
$$ W \gets W - \eta \frac{1}{\sqrt{h}} \frac{\partial	L}{\partial W} $$

> 在更新参数时，通过乘以 `1/sqrt(h)`，就可以调整学习的尺度。这意味着，参数的元素中变动较大(被大幅更新)的元素的学习率将变小。也就是说，可以按参数的元素进行学习率衰减，使变动大的参数的学习率逐渐减小。


![image.png](https://cdn.hashnode.com/res/hashnode/image/upload/v1661328785222/Pl3qf5zg6.png align="left")

### Adam

Adam 是 2015 年提出的新方法。它的理论有些复杂，直观地讲，就是融合了 Momentum 和 AdaGrad 的方法。

![image.png](https://cdn.hashnode.com/res/hashnode/image/upload/v1661328884647/bGHW44Z40.png align="left")


![image.png](https://cdn.hashnode.com/res/hashnode/image/upload/v1661328912422/Rt7PUkCOl.png align="left")

## 权重初始值

### 随机的初始化值

当深度学习网络发生过拟合时，是由于权重参数过大导致的。如果要抑制过拟合，在设定权重初始值的时候就选择比较小的值。

那么如果将权重初始值设定为0，这并不是一个好主意。将权重参数设置为0，或者设置为一样的值，会导致后面层神经元会在前向推理时传递相同的值，意味着在反向传播的时候，所有权重参数会进行相同的更新。这使得神经网络拥有许多不同的权重的意义丧失了。为了防止“权重均一化”(严格地讲，是为了瓦解权重的对称结构)，必须随机生成初始值。

### 隐藏层的激活值的分布

* 当使用第一层网络权重标准差为1的高斯分布

![image.png](https://cdn.hashnode.com/res/hashnode/image/upload/v1661333529707/9nPzV-ug5.png align="left")

这里使用激活函数`sigmoid`函数是S型函数，随着输出不断地靠近0(或者靠近1)，它的导数的值逐渐接近0。因此，偏向0和1的数据分布会造成反向传播中梯度的值不断变小，最后消失。这个问题称为**梯度消失(gradient vanishing)**。

* 将权重的标准差设为 0.01，进行相同的实验

![image.png](https://cdn.hashnode.com/res/hashnode/image/upload/v1661333661143/ZVaXC2Ozy.png align="left")

 这次的激活值分布有所偏向，说明多个神经元会输出近似相同的值，有些神经元的存在是没有意义的，整个神经网络会**表现力受限**

** Xavier 初始值
在 Xavier Glorot 的论文中， 为了使各层的激活值呈现出具有相同广度的分布， 提出前一层的节点数越多，要设定为目标节点的初始值的权重尺度就越小。

如果前一层的节点数为 n，则初始值使用标准差为**1/sqrt(n)**的高斯分布 

![image.png](https://cdn.hashnode.com/res/hashnode/image/upload/v1661334002100/xpBssU0mM.png align="left")

** He 初始值

当激活函数使用 ReLU 时，一般推荐使用 ReLU 专用的初始值，也被称为"He初始值"。

当前一层的节点数为 n 时，He 初始值使用标准差为`sqrt(2/n)`的高斯分布。和 `Xavier初始值`相比，因为 ReLU 的负值区域的值 为 0，为了使它更有广度，所以需要 2 倍的系数。

## Batch Normalization
Batch Norm的思路是调整各层的激活值分布使其拥有适当广度，有以下优点:
* 可以使学习快速进行(可以增大学习率)。
* 不那么依赖初始值(对于初始值不用那么神经质)。
* 抑制过拟合(降低Dropout等的必要性)。

![image.png](https://cdn.hashnode.com/res/hashnode/image/upload/v1661337326734/0o8AzOPt2.png align="left")

## 正则化
### 过拟合
发生过拟合的原因，主要有以下两个:
1. 模型拥有大量参数、表现力强。
2. 训练数据少。

### 权值衰减

很多过拟合原本就是 因为权重参数取值过大才发生的。**权值衰减**通过在学习的过程中对大的权重进行惩罚，来抑制过拟合。为损失函数加上权重的平方范数(L2范数)。这样一来，就可以抑制权重变大。

### Dropout

Dropout 是一种在学习的过程中随机删除神经元的方法。训练时，随机选出隐藏层的神经元，然后将其删除。被删除的神经元不再进行信号的传递。

训练时，每传递一次数据，就会随机选择要删除的神经元。然后，测试时，虽然会传递所有的神经元信号，但是对于各个神经元的输出，要乘上训练时的删除比例后再输出


![image.png](https://cdn.hashnode.com/res/hashnode/image/upload/v1661337983966/ASAnh3m0T.png align="left")

```python
class Dropout:
    def __init__(self, dropout_ratio=0.5):
        self.dropout_ratio = dropout_ratio
        self.mask = None
    def forward(self, x, train_flg=True):
        if train_flg:
            # 训练时, 随机选择神经元, 并且将选择神经元序号保存
            self.mask = np.random.rand(*x.shape) > self.dropout_ratio
            return x * self.mask
        else:
            # 推理时，将每个值乘以保留系数
            return x * (1.0 - self.dropout_ratio)
    def backward(self, dout):
        # 反向传播时，使用mask选择保留的神经元进行
        return dout * self.mask
```
可以将 Dropout 理解为，通过在学习过程中随机删除神经元，从而每一次都让不同的模型进行学习。

##超参数的验证

调整超参数时，必须使用超参数专用的确认数据。用于调整超参 数的数据，一般称为验证数据(validation data)


超参数的范围只要“大致地指定”就可以了。所谓“大致地指定”，是指像0.001到1000这样，以“10的阶乘”的尺度指定范围(也表述为“用对数尺度(logscale)指定”)。
