【手搓算法】反向传播算法 C++代码实现

1 反向传播算法

1.1 反向传播算法的公式

反向传播算法包含四个方程,公式的推导这里就不给大家推了(网上有一大堆现成的推导博客),其实核心就是误差的定义以及求导的链式法则,下面我只列出反向传播的四个方程:
δ L = ∇ a C ⊙ σ ′ ( z L ) δ l = ( ( ω l + 1 ) T δ l + 1 ) ⊙ σ ′ ( z l ) ∂ C ∂ b j l = δ j l ∂ C ∂ ω j k l = a j l − 1 δ k l \begin{aligned} \delta^{L} &= \nabla_{a}C \odot \sigma'(z^{L}) \\ \delta^{l} &= ((\omega^{l+1})^{T} \delta^{l+1}) \odot \sigma'(z^{l}) \\ \frac {\partial C} {\partial b^{l}_{j}} &= \delta^{l}_{j} \\ \frac{\partial C}{\partial \omega^{l}_{jk}} &= a^{l-1}_{j} \delta^{l}_{k} \end{aligned} δLδlbjlCωjklC=aCσ(zL)=((ωl+1)Tδl+1)σ(zl)=δjl=ajl1δkl
式中, C C C代表损失函数, δ L \delta^{L} δL代表输出层的误差, δ l \delta^{l} δl代表输出层的误差, ω \omega ω代表权重, b b b代表偏置, z z z代表未激活前的值(上一层的输出乘上相应的权重求和后的值), a a a代表激活后的值, b j l b^{l}_{j} bjl代表第 l l l层中第 j j j个节点所对应的偏置, δ j l \delta^{l}_{j} δjl代表第 l l l层中第 j j j个节点所对应的误差, ω j k l \omega^{l}_{jk} ωjkl代表第 l − 1 l-1 l1层中第 j j j个节点与第 l l l层中第 k k k个节点之间的权重, a j l − 1 a^{l-1}_{j} ajl1代表第 l − 1 l-1 l1层中第 j j j个节点所对应的输出值。前两个公式是矩阵运算,后两个公式是具体到每一个值的计算。下面介绍反向传播算法的流程。

1.2 反向传播算法的流程

通过公式可知,在进行求解误差之前,我们需要得到 z z z a a a,所以需要先进行前向的计算,然后再利用公式对误差进行反向传播,如下:

反向传播算法
step1: 前向计算,得到 z l z^{l} zl a l a^{l} al
step2: 计算输出层误差: δ L = ∇ a C ⊙ σ ′ ( z L ) \delta^{L} = \nabla_{a}C \odot \sigma'(z^{L}) δL=aCσ(zL)
step3: 计算隐藏层误差: δ l = ( ( ω l + 1 ) T δ l + 1 ) ⊙ σ ′ ( z l ) \delta^{l} = ((\omega^{l+1})^{T} \delta^{l+1}) \odot \sigma'(z^{l}) δl=((ωl+1)Tδl+1)σ(zl)
step4: 计算参数的梯度:
∂ C ∂ b j l = δ j l ∂ C ∂ ω j k l = a j l − 1 δ k l \begin{aligned} \frac {\partial C} {\partial b^{l}_{j}} &= \delta^{l}_{j} \\ \frac{\partial C}{\partial \omega^{l}_{jk}} &= a^{l-1}_{j} \delta^{l}_{k} \end{aligned} bjlCωjklC=δjl=ajl1δkl

反向传播算法执行一次之后,我们就得到了所有层的权重与偏置的梯度,从而可以进行参数更新,训练我们的模型,下面是在一轮中(一个周期中),模型训练的流程:

模型一轮训练的流程
step1: 分割训练集为 m m m份训练数据,令 i = 0 i=0 i=0
step2: 取第 i i i份训练数据作为模型的输入
step3: 通过反向传播算法得到梯度
step4: 使用某种更新方法对参数进行更新
step5: i = i + 1 i=i+1 i=i+1,若 i < m i<m i<m,回到step2;否则结束一轮训练

下面根据上述的方程与流程进行反向传播算法的实现。

2 反向传播算法的代码实现

为了方便展示,我省略了一些代码实现的细节,如果需要完整代码,可以访问我的仓库。如果觉得代码对您有帮助的话,可以给我的仓库star一下。

2.1 网络层的定义

首先创建一个数据结构,用于定义每一层的相关参数:

struct Layer {
    vector<vector<double>> weights;  // 权重(上一层连到当前层)
    vector<double> bias;  // 偏置
    vector<double> input;  // 上一层的输出乘上对应权重求和后的结果(未经过激活函数)
    vector<double> out;  // 经过激活函数后当前层的输出
    vector<double> error;  // 当前层的误差
    vector<vector<double>> weights_grad;  // 权重的梯度
    vector<double> bias_grad;  // 偏置的梯度
};

2.2 模型的定义

将上面定义的网络层集合到一起,形成模型,同时使用一些函数使得能够清晰地访问我们需要的参数
(这里请不用纠结为什么是layer-1,这和构造函数中对模型的初始化有关,为了尽可能只体现反向传播算法的相关内容,这里舍去构造函数的定义,详见完整代码):

class Network {
private:
    int layers_num;  // 隐藏层层数+1(隐藏层+输出层)
    vector<Layer> layers;  // 存储隐藏层和输出层
    vector<int> nodes_num_per_layer;  // 网络每一层的节点数,包括输入层
    vector<double> data;  // 训练数据(在构造函数中构建)
    vector<double> label;  // 训练标签(在构造函数中构建)
    int data_size = 1000;  // 训练集的大小
    double lr;  // 学习率
    int epochs;  // 训练周期
public:
	inline double get_weight(int layer, int i, int j) {  // 第layer层中,上一层第i个节点连接到当前层第j个节点的权重值
        return layers[layer - 1].weights[i][j];
    }
    inline double get_bias(int layer, int j) {  // 第layer层中,第j个节点的偏置
        return layers[layer - 1].bias[j];
    }
    inline double get_input(int layer, int j) {  // 第layer层中,第j个节点的输入
        return layers[layer - 1].input[j];
    }
    inline double get_output(int layer, int j) {  // 第layer层中,第j个节点的输出
        return layers[layer - 1].out[j];
    }
    inline double get_error(int layer, int j) {  // 第layer层中,第j个节点的误差
        return layers[layer - 1].error[j];
    }
    inline double get_weight_grad(int layer, int i, int j) {  // 第layer层中,上一层第i个节点连接到当前层第j个节点的权重的梯度
        return layers[layer - 1].weights_grad[i][j];
    }
    inline double get_bias_grad(int layer, int j) {  // 第layer层中,第j个节点的偏置的梯度
        return layers[layer - 1].bias_grad[j];
    }
    ...
    ...
    ...
};

2.3 前向计算

由上述可知,反向传播之前需要先进行前向的计算过程,所以这里定义前向的计算过程(注意,下述代码所给的计算过程是针对于单样本而言的):

class Network {
    ...
    ...
    ...
    void forward(int index) {
        // 从输入层到第一层
        for (int i = 0; i < nodes_num_per_layer[1]; i++) {
            layers[0].input[i] = data[index] * get_weight(1, 0, i) + get_bias(1, i);
            layers[0].out[i] = activate(get_input(1, i), active_type);  // activate是激活函数
        }

        // 各隐藏层和输出层
        for (int n = 2; n <= layers_num; n++) {
            for (int i = 0; i < nodes_num_per_layer[n]; i++) {  // 当前层
                layers[n - 1].input[i] = 0;
                // 对上一层的结果加权求和
                for (int j = 0; j < nodes_num_per_layer[n - 1]; j++) {  // 上一层
                    layers[n - 1].input[i] += get_output(n - 1, j) * get_weight(n, j, i);
                }
                layers[n - 1].input[i] += get_bias(n, i);
                // 将z输入激活函数
                layers[n - 1].out[i] = activate(get_input(n, i), active_type);
            }
        }
    }
};

2.4 反向传播

计算完前向过程之后就可以进行反向传播,先利用第一个方程计算输出层的误差,再利用第二个方程将误差从输出层反向传播回每一层,同样是针对单个样本的反向传播:

class Network {
    ...
    ...
    ...
    void backward(int index) {
        // 计算输出层的误差
        // activate_det为激活函数的导数
        layers[layers_num - 1].error[0] = (get_output(layers_num, 0) - label[index]) * activate_det(get_input(layers_num, 0), active_type);  // 第一个方程

        // 计算隐藏层的误差
        for (int n = layers_num - 1; n >= 1; n--) {
            for (int i = 0; i < nodes_num_per_layer[n]; i++) {  // 当前层
                layers[n - 1].error[i] = 0;
                for (int j = 0; j < nodes_num_per_layer[n + 1]; j++) {  // 下一层
                    layers[n - 1].error[i] += get_weight(n + 1, i, j) * get_error(n + 1, j) * activate_det(get_input(n, i), active_type);
                }
            }
        }
    }
};

2.5 更新参数

这里使用随机梯度下降对参数进行更新:
(完整代码中还包含一些其他优化器的更新方式)

class Network {
    ...
    ...
    ...
    void update_sgd(int index) {
        // 更新第一层的权重
        for (int j = 0; j < nodes_num_per_layer[1]; j++) {
            layers[0].weights[0][j] -= lr * get_error(1, j) * data[index];
        }
        // 更新隐藏层的权重
        for (int n = 2; n <= layers_num; n++) {
            for (int j = 0; j < nodes_num_per_layer[n]; j++) {  // 当前层
                for (int i = 0; i < nodes_num_per_layer[n - 1]; i++) {  // 上一层
                    layers[n - 1].weights[i][j] -= lr * get_error(n, j) * get_output(n - 1, i);
                }
            }
        }
        // 更新所有层的偏置
        for(int n = 1; n <= layers_num; n++) {
            for (int i = 0; i < nodes_num_per_layer[n]; i++) {
                layers[n - 1].bias[i] -= lr * get_error(n, i);
            }
        }
    }

};

2.6 训练

最后如1.2节中所展示的,进行完整的训练流程:

class Network {
    ...
    ...
    ...
    void train() {
        for (int epoch = 0; epoch < epochs; epoch++) {
            cout << epoch << endl;
            // 单样本进行训练
            for (int index = 0; index < data.size(); index++) {
                forward(index);
                backward(index);
                update_sgd(index);
            }
        }
    }
};

至此,反向传播算法与训练流程已经实现完毕。
如果有任何问题,欢迎在评论区批评指正,不胜感激!!!

  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值