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

上一篇文章中,我们加载了预先训练好的手写数字识别模型的权重和偏置,这一篇文章中会剖析下如何从数据中让机器自动学习得到模型参数。
从数据中学习
在传统的机器学习中,是需要人工从数据中提取出可以从输入数据(图像)中提取重要数据的转换器。然后使用这些特征量,将输入数据转换为向量,然后使用机器学习算法(SVM, KNN)等,进行学习,如下图。

对于深度学习,可以直接使用原始数据,进行“端到端”的学习。这里的“端到端”指的是从原始数据到目标结果的意思,因此深度学习也叫端到端的机器学习。
损失函数
在深度学习网络的训练过程中,目的就是以损失函数为基准,寻找能使它的值达到最小的参数(权重、偏置)
为什么要设置损失函数
既然神经网络以提高识别精度为目的,那么为什么不以识别精度为优化目标呢? 这是因为使用识别精度为目标,会使对权重参数求导,大部分地方的导数都是0,无法进行参数更新。
为什么以权重参数对识别精度求导,大多数地方会得到0? 这是因为识别精度是一个离散值,比方说100张图片里识别了32张,此时识别精度为32%,如果细微更新参数,识别精度仍未32%,无法体现模型受参数更新带来的影响。
同样的道理,如果使用阶跃函数作为激活函数,神经网络的学习也无法进行。参数的微小的变化会被不连续变化(存在导数为0)的阶跃函数抹杀,导致损失函数的值不会发生任何变化。
均方误差

交叉熵误差
在实际实现时,要将 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+delta 和 x-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;
}
神经网络的梯度

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




