# 《深度学习入门》的cpp实现-ch04: 神经网络的学习

上一篇文章中，我们加载了预先训练好的手写数字识别模型的权重和偏置，这一篇文章中会剖析下如何从数据中让机器自动学习得到模型参数。

## 从数据中学习

在传统的机器学习中，是需要人工从数据中提取出可以从输入数据(图像)中提取重要数据的转换器。然后使用这些特征量，将输入数据转换为向量，然后使用机器学习算法(SVM, KNN)等，进行学习，如下图。

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

对于深度学习，可以直接使用原始数据，进行“端到端”的学习。这里的“端到端”指的是从原始数据到目标结果的意思，因此深度学习也叫端到端的机器学习。

## 损失函数

在深度学习网络的训练过程中，目的就是以损失函数为基准，寻找能使它的值达到最小的参数(权重、偏置)

### 为什么要设置损失函数

既然神经网络以提高识别精度为目的，那么为什么不以识别精度为优化目标呢？ 这是因为使用识别精度为目标，会使对权重参数求导，大部分地方的导数都是0，无法进行参数更新。

为什么以权重参数对识别精度求导，大多数地方会得到0？ 这是因为识别精度是一个离散值，比方说100张图片里识别了32张，此时识别精度为32%，如果细微更新参数，识别精度仍未32%，无法体现模型受参数更新带来的影响。

同样的道理，如果使用阶跃函数作为激活函数，神经网络的学习也无法进行。参数的微小的变化会被不连续变化(存在导数为0)的阶跃函数抹杀，导致损失函数的值不会发生任何变化。


### 均方误差

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

### 交叉熵误差

![image.png](https://cdn.hashnode.com/res/hashnode/image/upload/v1660479989537/rtpdoSm_E.png align="left")
在实际实现时，要将 log 内数字增加一个微小值，防止 log(0) 为负无穷大的情况出现。

mini-batch版本
```cpp

double cross_entropy_error(Eigen::MatrixXd y, Eigen::MatrixXi labels) {
    double sum = 0;
    for (int i = 0; i < y.rows(); i++) {
        double value = y(i, labels(0, i));
        sum += std::log(value + 0.001f); // +0.001f 防止log(0)这种inf的出现
    }
    return -1.0f * sum / double(y.rows());
}
```

## 梯度更新
### 求导的方法
求导的方法有两种，一种是基于数学式推导的**解析性求导**，一种是根据微小的查分求导的**数值微分**。我们这里使用**数值微分**进行求导。

### 偏导数
$$f(x_0, x_1) = x^2_0 + x^2_1$$
对于拥有多个变量的函数的导数称为**偏导数**， 可以写为
$$\frac{\partial y}{\partial x_0}, \frac{\partial y}{\partial x_1}$$ 

偏导数和单变量的导数一样，都是求某个地方的斜率。不过， 偏导数需要将多个变量中的某一个变量定为目标变量，并将其他变量固定为 某个值。


### 梯度
像 $$\frac{\partial y}{\partial x_0}, \frac{\partial y}{\partial x_1}$$  全部由偏导数形成的向量称为梯度。 求值方式为对于某个点求 `x+delta` 和 `x-delta` ，其他变量不变，带入函数的值后微分。
C++实现
```cpp
Eigen::MatrixXd TwoLayerModel::numerical_gradient(Eigen::MatrixXd& mat) {
    Eigen::MatrixXd mat_grad = Eigen::MatrixXd::Zero(mat.rows(), mat.cols());
    for (int i = 0; i < mat.rows(); i++) {
        for (int j = 0; j < mat.cols(); j++) {
            double origin_value = mat(i, j);
            mat(i, j) = origin_value - delta;
            double h1 = loss(forward(), batch_labels);

            mat(i, j) = origin_value + delta;
            double h2 = loss(forward(), batch_labels);

            double diff = (h2 - h1) / (2 * delta);
            mat_grad(i, j) = diff;
//            std::cout << "mat_grad(" << i << ", " << j << "): " << diff << " h1: " << h1 << " h2: " << h2 << std::endl;
            mat(i, j) = origin_value;
        }
    }
    return mat_grad;
}
```

### 神经网络的梯度

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


## 学习算法的实现
* 步骤 1(mini-batch) 从训练数据中随机选出一部分数据，这部分数据称为 mini-batch。我们 的目标是减小 mini-batch 的损失函数的值。

* 步骤 2(计算梯度)
为了减小 mini-batch 的损失函数的值，需要求出各个权重参数(W1, W2, b1, b2)的梯度。 梯度表示损失函数的值减小最多的方向。

* 步骤 3(更新参数) 将权重参数沿梯度方向进行微小更新(需要乘以学习率)。

* 步骤 4(重复)
重复步骤 1、步骤 2、步骤 3。


## 关于效率的吐槽

不得不说，通过微分求神经网络最优权重实在是太慢了！ 在我本地运行的两层神经网络，参数形状是W1(784, 40)、W2(50, 10)、b1(1, 50)、b2(1, 10)。 每次求权重对损失函数的梯度，就需要计算 780 * 40 + 50 * 10 + 50 + 10 = 31760 个点的导数，每次求导就要进行两次前向推理。

何况每个batch有100个，要进行1000次迭代。光前向推理就需要进行 30亿次，实在是太慢了，不过下一章会有更高效的方式来进行神经网络参数的学习。

代码在[这里](https://github.com/qilingzhao/dlfs_cpp/blob/master/src/ch04/nn_study.cpp)
