Skip to main content

Command Palette

Search for a command to run...

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

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

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

从数据中学习

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

image.png

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

损失函数

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

为什么要设置损失函数

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

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

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

均方误差

image.png

交叉熵误差

image.png 在实际实现时,要将 log 内数字增加一个微小值,防止 log(0) 为负无穷大的情况出现。

mini-batch版本


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+deltax-delta ,其他变量不变,带入函数的值后微分。 C++实现

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

学习算法的实现

  • 步骤 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亿次,实在是太慢了,不过下一章会有更高效的方式来进行神经网络参数的学习。

代码在这里

More from this blog

<Programming with Types>随想: Chapter 2. Basic types

类型限制了一个变量可以接受的有效值的集合,对数据可以进行的操作,数据的意义。 空类型(The empty type) 根据类型的定义,类型定义了可以接受的有效值集合,那么这个集合有没有可能为空?答案是有可能的,TypeScirpt 的 never 就是这种类型。 需要注意的是,空类型不同与 void, 后者是有效值集合当中只有一个值,但这个值没有任何意义。而空类型的有效值集合本身是空的。 使用场景 控制流分析 在函数调用时,标志一个函数不会返回任何值: 在调用过程中抛出异常、死循环或者程序崩溃...

Mar 5, 2023
<Programming with Types>随想: Chapter 2. Basic types

精读《设计机器学习系统》-ch04: 训练数据

不同于 Chapter03 从系统的角度来处理数据,这一章从数据科学的视角来处理数据。这章的标题是“training Data”,而非“training dataset”,因为 数据集(dataset) 意味着有限(finite)和固定(stationary), 而现实生产环境中的 数据(data) 通常是 无限 并且 不固定 的。 抽样 抽样方法在 ML 项目的生命周期中无处不在,在这一节中,我们使用生成训练数据作为例子。 那为什么需要抽样?直接使用全部数据不可以吗? 首先,在现实世界中,并不...

Dec 10, 2022
精读《设计机器学习系统》-ch04: 训练数据
7

70 Talk

19 posts