Skip to main content

Command Palette

Search for a command to run...

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

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

上一节使用梯度更新的方法求神经网络的参数的梯度,虽然可行,但是耗时很多。这篇文章我们使用计算图(computational graph) 来进行计算损失函数对神经网络参数的梯队。

计算图

计算图是将计算过程用图形表示出来,将计算的的从左向右计算, 即由数据推理出结果,称为正向传播;将从右向左的传播,即由损失函数计算参数梯度,称为反向传播

好处

  1. 使用计算图可以聚焦于局部计算,无论全局计算多么复杂,各个步骤只需要对输入输出做简单的计算。通过节点(算子)间的传递,可以获取全局复杂计算的结果。

  2. 使用计算图最大的好处,可以利用反向传播高效计算导数。可以根据链式法则进行反向传播,求出损失函数关于多个层参数的梯度,并且中间求导结果可以被多个下游共享。

如果想要求损失函数对于一个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

image.png

// 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

image.png

// 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

image.png

// 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

image.png

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, 即仿射变换。几何中,仿射变换包括一次线性变换和一次平移,分别对应神经网络的加权和运算与加偏置运算。

image.png

正向传播时,偏置会被加到每一个数据(第 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逐步增加 image.png

image.png

实现代码

完整代码见: computational_graph.cpp

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