《深度学习入门》的cpp实现-ch05: 误差反向传播法

上一节使用梯度更新的方法求神经网络的参数的梯度,虽然可行,但是耗时很多。这篇文章我们使用计算图(computational graph) 来进行计算损失函数对神经网络参数的梯队。
计算图
计算图是将计算过程用图形表示出来,将计算的的从左向右计算, 即由数据推理出结果,称为正向传播;将从右向左的传播,即由损失函数计算参数梯度,称为反向传播。
好处
使用计算图可以聚焦于局部计算,无论全局计算多么复杂,各个步骤只需要对输入输出做简单的计算。通过节点(算子)间的传递,可以获取全局复杂计算的结果。
使用计算图最大的好处,可以利用反向传播高效计算导数。可以根据链式法则进行反向传播,求出损失函数关于多个层参数的梯度,并且中间求导结果可以被多个下游共享。
如果想要求损失函数对于一个mn的矩阵的梯度,需要依次改变矩阵中数量为mn个位置的值,每改变一次需要进行两次(x-delta 和 x+delta)前向推理。 而对于误差反向传播法,只需要进行一次正向传播和反向传播,即可求出整个矩阵的梯度。 换言之,使用计算图计算损失函数梯度,需要进行的传播次数和矩阵规模无关🎉。
链式法则
如果某个函数由复合函数表示,则该复合函数的导数可以用构成复合函数的各个函数的导数的乘积表示。 即 $$z=t^2$$ $$t=x+y$$ 的情况下 $$ \frac{\partial z}{\partial x} = \frac{\partial z}{\partial t} \frac{\partial t}{\partial x} $$
简单层的实现
AddOp

// AddOp
Eigen::MatrixXf AddOp::forward(const Eigen::MatrixXf& a, const Eigen::MatrixXf& b) const {
return a + b;
}
Eigen::MatrixXf AddOp::backward(const Eigen::MatrixXf &dOut) const {
return dOut;
}
MulOp

// MulOp
Eigen::MatrixXf MulOp::forward(const Eigen::MatrixXf &a, const Eigen::MatrixXf &b) {
this->a = a;
this->b = b;
return a * b;
}
std::pair<Eigen::MatrixXf, Eigen::MatrixXf> MulOp::backward(const Eigen::MatrixXf &dOut) const {
return std::make_pair(dOut * this->b, dOut * this->a);
}
激活函数层
ReLUOp

// ReLUOp
Eigen::MatrixXf ReLUOp::forward(const Eigen::MatrixXf &xx) {
this->x = xx;
Eigen::MatrixXf res = (this->x.array() < 0.f).select(0.f, xx);
return res;
}
Eigen::MatrixXf ReLUOp::backward(const Eigen::MatrixXf &dOut) const {
Eigen::MatrixXf res = (this->x.array() < 0.f).select(0.f, dOut);
return res;
}
SigmoidOp

class Sigmoid:
def __init__(self):
self.out = None
def forward(self, x):
out = 1 / (1 + np.exp(-x)) self.out = out
return out
def backward(self, dout):
dx = dout * (1.0 - self.out) * self.out
return dx
AffineOp
Affine, 即仿射变换。几何中,仿射变换包括一次线性变换和一次平移,分别对应神经网络的加权和运算与加偏置运算。

正向传播时,偏置会被加到每一个数据(第 1 个、第 2 个......)上。因此, 反向传播时,各个数据的反向传播的值需要汇总为偏置的元素。
声明
class AffineOp {
private:
Eigen::MatrixXf x;
Eigen::MatrixXf w;
Eigen::MatrixXf b;
Eigen::MatrixXf dW;
Eigen::MatrixXf dB;
public:
AffineOp() {};
// 之所以Affine需要构造函数,而relu, sigmoid这种不需要
// 因为在图中流动的是x(图像矩阵), 经过一次正向和反向传播求出dW和dB, 然后对w和b进行微调.
// 对于relu, 如果有个参数控制x>0时的斜率q, y = x > 0 ? qx : 0; 那么这个q也是需要在初始化时传入的。
// 其实, AddOp, MulOp 是 AffineOp 的一种特化.
AffineOp(Eigen::MatrixXf w, Eigen::MatrixXf b) : w(std::move(w)), b(std::move(b)) {};
Eigen::MatrixXf forward(const Eigen::MatrixXf& x);
Eigen::MatrixXf backward(const Eigen::MatrixXf& dOut);
friend class TwoLayerNet;
};
实现代码
// AffineOp
Eigen::MatrixXf AffineOp::forward(const Eigen::MatrixXf& x) {
this->x = x;
Eigen::MatrixXf temp = x * this->w;
for (int i = 0; i < temp.rows(); i++) {
for (int j = 0; j < temp.cols(); j++) {
temp(i, j) += this->b(0, j);
}
}
return temp;
}
Eigen::MatrixXf AffineOp::backward(const Eigen::MatrixXf &dOut) {
this->dW = this->x.transpose() * dOut;
this->dB = dOut.colwise().sum();
return dOut * this->w.transpose();
}
SoftmaxWithLossOp
float SoftmaxWithLossOp::forward(const Eigen::MatrixXf& x, const Eigen::MatrixXi& labels) {
this->t = labels;
this->y = softmax(x);
this->loss = cross_entropy_error(this->y, this->t);
return this->loss;
}
Eigen::MatrixXf SoftmaxWithLossOp::backward() {
int batch_size = this->t.size();
Eigen::MatrixXf dX(this->y);
for (int i = 0; i < dX.rows(); i++) {
dX(i, this->t(0, i)) = dX(i, this->t(0, i)) - 1;
dX.row(i) /= float (batch_size);
}
return dX;
}
误差反向传播法的学习过程
设计一个具有两层的神经网络,第一层的激活函数的ReLU, 损失函数为SoftMaxWithLoss
for (int i = 0; i < TwoLayerNet::iter_times; i++) {
std::cout << "iter cnt: " << i << std::endl;
// 加载随机数据, 对于SGD的'S'
net.load_batch_data();
// 一次前向推理
Eigen::MatrixXf predictOut = net.predict();
// 计算准确率
float acc = net.acc(predictOut);
// 计算损失函数值
float loss = net.loss(predictOut);
// 求梯度
net.gradient();
// 使用梯度更新神经网络参数(Affine层的权重和偏置)
net.learn();
}
效果数据
经过1000次的训练,loss逐渐降低,acc逐步增加


实现代码
完整代码见: computational_graph.cpp




