python编程---不调包实现后向传播算法

原理介绍

写上了,待我补充上来,绝对不长,这里注重编码实现,所只介绍关键数据!!!

首先,前向传播:

s ( 1 ) = W T ( 1 ) X ( 0 ) − b ( 1 ) X ( 1 ) = ϕ ( s ( 1 ) ) s ( 2 ) = W T ( 1 ) X ( 1 ) − b ( 2 ) X ( 2 ) = ϕ ( s ( 2 ) ) \begin{aligned} s^{(1)} &= W^{T(1)}X^{(0)} - b^{(1)} & \quad X^{(1)} = \phi(s^{(1)}) \\ s^{(2)} &= W^{T(1)} X^{(1)} - b^{(2)} & \quad X^{(2)} = \phi(s^{(2)}) \end{aligned} s(1)s(2)=WT(1)X(0)b(1)=WT(1)X(1)b(2)X(1)=ϕ(s(1))X(2)=ϕ(s(2))

其次,后向传播,更新参数:

后向后弦传播关于 s s s的导数,也即 ∂ C ∂ s \frac{\partial C}{\partial s} sC
先写出一个通用的:
δ ( L ) = ( X L − Z ) ⊙ ϕ ′ ( s ( L ) ) δ ( l − 1 ) = ( W ( l ) s ( l ) ) ⊙ ϕ ( s ( l − 1 ) ) \begin{aligned} \delta^{(L)} = (X^{L} - Z) \odot \phi'(s^{(L)}) \\ \delta^{(l-1)} = (W^{(l)} s^{(l)}) \odot \phi(s^{(l-1)}) \\ \end{aligned} δ(L)=(XLZ)ϕ(s(L))δ(l1)=(W(l)s(l))ϕ(s(l1))
其中 δ ( L ) = ∂ C ∂ s ( L ) , δ ( l − 1 ) = ∂ C ∂ s ( l − 1 ) \delta^{(L)} = \frac{\partial C}{\partial s^{(L)}}, \delta^{(l-1)} = \frac{\partial C}{\partial s^{(l-1)}} δ(L)=s(L)C,δ(l1)=s(l1)C. C C C是你所选择的损失函数,这里我选择的损失函数是平方损失函数,即 C = ( Y − Z ) 2 C = (Y - Z)^2 C=(YZ)2 Y Y Y是你前向传播得到的一个输出的估计, Z Z Z是你的理想输出,也就是你需要模拟的真实值的输出.

再写一下这里我用的两层的(是前面通用的一个两层实例):
δ ( 2 ) = ( X 2 − Z ) ⊙ ϕ ′ ( s ( 2 ) ) δ ( 1 ) = ( W ( 2 ) s ( 2 ) ) ⊙ ϕ ( s ( 1 ) ) \begin{aligned} \delta^{(2)} = (X^{2} - Z) \odot \phi'(s^{(2)}) \\ \delta^{(1)} = (W^{(2)} s^{(2)}) \odot \phi(s^{(1)}) \\ \end{aligned} δ(2)=(X2Z)ϕ(s(2))δ(1)=(W(2)s(2))ϕ(s(1))
刚介绍的 δ ( L ) \delta^{(L)} δ(L) δ ( l − 1 ) \delta^{(l-1)} δ(l1)都是为了后来求损失函数关于未知参数系数向量 ω \omega ω和偏置 b b b而作准备的。

下面继续介绍损失函数关于 W W W b b b的偏导数,这一步是为了后面适用梯度下降法更新参数 W W W b b b做准备的。
∂ C ∂ W ( l ) = X ( l − 1 ) δ ( l ) T ∂ C ∂ B ( l ) = − δ ( l ) \begin{aligned} &\frac{\partial C}{\partial W^{(l)}} = X^{(l-1)} \delta^{(l)T} \\ &\frac{\partial C}{\partial B^{(l)}} = -\delta^{(l)}\\ \end{aligned} W(l)C=X(l1)δ(l)TB(l)C=δ(l)
下面继续按照前面的套路写一下损失函数 C C C关于第二层和第一层的 W W W B B B求偏导的过程:
∂ C ∂ W ( 2 ) = ∂ C ∂ s ( 2 ) ⋅ ∂ s ( 2 ) ∂ W ( 2 ) = X ( 1 ) δ ( 2 ) T ∂ C ∂ b ( 2 ) = ∂ C ∂ s ( 2 ) ⋅ ∂ s ( 2 ) ∂ b ( 2 ) = − δ ( 2 ) ∂ C ∂ W ( 1 ) = ∂ C ∂ s ( 1 ) ⋅ ∂ s ( 1 ) ∂ W ( 1 ) = X ( 0 ) δ ( 1 ) T ∂ C ∂ b ( 1 ) = ∂ C ∂ s ( 1 ) ⋅ ∂ s ( 1 ) ∂ b ( 1 ) = − δ ( 1 ) \begin{aligned} & \cfrac{\partial C}{\partial W^{(2)}} = \cfrac{\partial C}{\partial s^{(2)}} \cdot \cfrac{\partial s^{(2)}}{\partial W^{(2)}} = X^{(1)} \delta^{(2)T} \\ & \cfrac{\partial C}{\partial b^{(2)}} = \cfrac{\partial C}{\partial s^{(2)}} \cdot \cfrac{\partial s^{(2)}}{\partial b^{(2)}} = - \delta^{(2)} \\ & \cfrac{\partial C}{\partial W^{(1)}} = \cfrac{\partial C}{\partial s^{(1)}} \cdot \cfrac{\partial s^{(1)}}{\partial W^{(1)}} = X^{(0)} \delta^{(1)T} \\ & \cfrac{\partial C}{\partial b^{(1)}} = \cfrac{\partial C}{\partial s^{(1)}} \cdot \cfrac{\partial s^{(1)}}{\partial b^{(1)}} = - \delta^{(1)} \\ \end{aligned} W(2)C=s(2)CW(2)s(2)=X(1)δ(2)Tb(2)C=s(2)Cb(2)s(2)=δ(2)W(1)C=s(1)CW(1)s(1)=X(0)δ(1)Tb(1)C=s(1)Cb(1)s(1)=δ(1)
其中梯度下降法是这样更新的:
在这里插入图片描述
这里的 f f f是一个关于 x x x的函数。对应到我们这里就是:
W ( n + 1 ) = W ( n ) − η ∂ C ∂ W b ( n + 1 ) = b ( n ) − η ∂ C ∂ b \begin{aligned} W(n+1) = W(n) - \eta \cfrac{\partial C}{\partial W} \\ b(n+1) = b(n) - \eta \cfrac{\partial C}{\partial b} \end{aligned} W(n+1)=W(n)ηWCb(n+1)=b(n)ηbC
之前已经计算出了偏导,将偏导的值代入我们就可以使用梯度下降法来更新 W W W b b b啦:
W ( 2 ) ( n + 1 ) = w ( 2 ) ( n ) − η ⋅ X ( 1 ) ( n ) ⋅ δ ( 2 ) ( n ) b ( 2 ) ( n + 1 ) = b ( 2 ) ( n ) + η ⋅ δ ( 2 ) ( n ) W ( 1 ) ( n + 1 ) = w ( 1 ) ( n ) − η ⋅ X ( 0 ) ( n ) ⋅ δ ( 1 ) ( n ) b ( 1 ) ( n + 1 ) = b ( 1 ) ( n ) + η ⋅ δ ( 1 ) ( n ) \begin{aligned} & W^{(2)}(n+1) = w^{(2)}(n) - \eta \cdot X^{(1)}(n) \cdot\delta^{(2)}(n) \\ & b^{(2)}(n+1) = b^{(2)}(n) + \eta \cdot \delta^{(2)}(n) \\ & W^{(1)}(n+1) = w^{(1)}(n) - \eta \cdot X^{(0)}(n) \cdot\delta^{(1)}(n) \\ & b^{(1)}(n+1) = b^{(1)}(n) + \eta \cdot \delta^{(1)}(n) \\ \end{aligned} W(2)(n+1)=w(2)(n)ηX(1)(n)δ(2)(n)b(2)(n+1)=b(2)(n)+ηδ(2)(n)W(1)(n+1)=w(1)(n)ηX(0)(n)δ(1)(n)b(1)(n+1)=b(1)(n)+ηδ(1)(n)
到这里,前向传播和后向传播的计算理论已经结束了。

此外,我还想添加一些东西:

损失函数的选择,样本输入,更新参数的问题

在我自己写代码的过程中,发现输入 N N N个训练样本不知道怎么传到神经网络中去,困惑点在于,两点:

  1. 我是一个样本进入一次网络,前向传播输出值,然后后向传播更新参数,每次都按照当前的梯度下降的方法来更新很多次参数;
  2. 还是说先我把输入就当作是N个神经元,这样来针对所有的输入来更新参数呢?

答案是什么?
两者都不是!!!
那到底是什么呀?
呜呜,我也不知道,我还要回去看我的代码,呜呜
呜呜,我知道了,关键点在于设置损失函数,它取为MSE,说我也说不清楚,我写个公式:
C o s t = M S E = 1 2 N ∑ i = 1 N ( Y i − Z i ) 2 Cost = MSE = \cfrac{1}{2N} \sum\limits_{i=1}^{N}(Y_i - Z_i)^2 Cost=MSE=2N1i=1N(YiZi)2
均方误差的话,之前的分母上为啥要加上一个2呀?
之前在后向传播的时候用到了 C C C关于 W W W b b b的求导,求导,一个平方项有一个2,乘以前面的 1 2 \frac{1}{2} 21不就使得前面的常数系数为1了,多舒服。在代码中我有写了,只是有些蠢罢了,待我以后更新!!!

载入数据

utils.py文件中

"""
生成数据
"""

import numpy as np
import pandas as pd

def load_data(n=1000):
    np.random.seed(0)
    X = np.random.randn(n) * 10
    Z = np.tan(X)
    # X: input, Z: target ouput
    return X, Z

前向传播实现

暂时在bp-matrix.py

"""
1. 初始化网络结构,参数、训练函数,训练次数
2. 输入训练样本,训练网络
3. 前向过程,对比实际输出与期望输出,计算MSE
4. 后向传播过程,使用梯度下降法,调整网络参数
5. 调整参数之后,计算MSE
6. 指定终止条件,如果满足,停止,不满足到第 3步骤
"""


import numpy as np
from numpy.core.defchararray import replace
import pandas as pd
from utils import *
import matplotlib.pyplot as plt
plt.rcParams['font.sans-serif'] = ['SimHei']
plt.rcParams['axes.unicode_minus'] = False

class NotMatch(Exception):
    pass


class BackPropagation:
    def __init__(self, X, Z):
        self.X = np.array(X)
        self.Z = np.array(Z)
        self.init_paras()

    # 初始化权重、偏差(阈值)、训练的次数、学习率、episilon
    def init_paras(self):
        self.layer_size = [1, 10, 1] # 输入一个,输出一个,两层,中间隐藏层有10个(3个)隐藏的神经元
        self.L = len(self.layer_size) - 1

        self.Weights = []
        self.Bias = []
        for i in range(len(self.layer_size)-1):
            self.Weights.append(
                (
                    np.random.randn(self.layer_size[i]
                    * self.layer_size[i+1]) * 1 / np.sqrt(self.layer_size[i] / 2)
                    ).reshape(self.layer_size[i], self.layer_size[i+1])
            )
            self.Bias.append((np.random.randn(self.layer_size[i+1])).reshape(-1, 1))

        self.time_lim = 200
        self._active = self.sig
        self.eta = 0.15
        self.epsilon = 0.1
    
    # sigmoid 激活函数
    def sig(self, x):
        return 1 / (1 + np.exp(-x))

    # ReLU 激活函数
    def ReLU(self, x):
        return np.vectorize(lambda x: max(x, 0))(x)

    # 对激活函数求导
    def de_phi(self, x):
        if self._active == self.ReLU:
            return np.vectorize(lambda x: 1 if x >=0 else 0)(x)
        elif self._active == self.sig:
            return self.sig(x) * (1 - self.sig(x))

    def unit_loss(self, Y, Z):
        return 0.5 * np.sqrt(np.sum((Y-Z) ** 2))

    # 损失函数,这里是MSE
    def loss(self, Y, Z, w_i, b_i):
        Y = np.array(Y)
        Z = np.array(Z)
        # 气死我了,我居然不会适用这个NotMach异常!!!
        # if len(Y) != len(Z):
        #     raise NotMatch("Y and Z must be the same length!")
        actual_loss = 0
        for i, x_i in enumerate(self.X):
            _, x_out = self.forward_net(x_i, w_i, b_i)
            actual_loss += self.unit_loss(x_out[-1], self.Z[i])
        actual_loss = actual_loss / len(self.X)
        actual_loss = actual_loss

        return actual_loss

    # 前向传播,仅仅计算输出值
    def forward_net(self, X, w_i, b_i):
        # 变成二维,方便之后进行矩阵运算
        try:
            # 输入是一维数据
            X = np.array(X).reshape(1, 1)
        except:
            # 输入是多维数据
            X = np.array(X).reshape(-1, 1)

        S = []
        X_out = []
        for l in range(self.L):
            # l = l + 1
            # 此处的l是0, 1
            # 实际上是1, 2
            if l == 0:
                s_l = (w_i[l].T @ X - b_i[l]).ravel()
            else:
                s_l = (w_i[l].T @ X_out[-1] - b_i[l]).ravel()
            X_1 = self._active(s_l)
            S.append(s_l)
            X_out.append(X_1)
        
        return S, X_out

    # 后向传播
    # 损失函数是所有输入数据的MSE
    def backward_net(self, w_i, b_i):
        # 初始化delta, C_de_W, C_de_b
        delta = [0] * self.L
        C_de_W = [0] * self.L
        C_de_b = [0] * self.L

        for i, x_i in enumerate(self.X):
            S, X_out = self.forward_net(x_i, w_i, b_i)
            for l in range(self.L-1, -1, -1):
                if l == self.L - 1:
                    delta_l = (X_out[l] - self.Z[i]) * self.de_phi(S[l])
                    delta_l = delta_l.reshape(-1, 1)
                else:
                    delta_l = (w_i[-1] @ delta[-1]).T * self.de_phi(S[l])
                    delta_l = delta_l.reshape(len(delta_l.ravel()), 1)
                delta[l] = delta_l

                # 计算关于w, b 的偏导
                X_out_tmp = X_out[l-1].reshape(-1, 1)
                C_de_W[l] += X_out_tmp @ delta_l.T
                C_de_b[l] += -delta_l
        
        # 按照1, ..., L 排序
        C_de_W = [C_de_Wi / len(self.X) for C_de_Wi in C_de_W]
        C_de_b = [C_de_bi / len(self.X) for C_de_bi in C_de_b]
        # 其实delta不用传到后面的计算中去, 那么, 不传
        return C_de_W, C_de_b
        
    # 更新权重w, b
    def update_wb(self):
        w_i = self.Weights
        b_i = self.Bias
        # 初始化后向传播,计算C_de_W, C_de_b
        # 这里的损失函数是输入数据的MSE
        C_de_W, C_de_b = self.backward_net(w_i, b_i)
        i = 1
        
        MSE_plot = []
        while i < self.time_lim: # 1, 2, ...
            # 更新权重
            for l in range(self.L-1, -1, -1): # l: 1, 0
                b_i[l] -= self.eta * C_de_b[l]
                w_i[l] -= self.eta * C_de_W[l]
            # S, X_out = self.forward_net(self.X, w_i, b_i)
            # 这里需要输出X_out, 将所有的输出的平方和都相加,即MSE

            # 判断是否要跳出循环
            MSE = self.loss(self.X, Z, w_i, b_i)
            if MSE < self.epsilon:
                break

            # 存储上一步计算得到的偏导,这里的偏导数是loss的MSE分别对W, b 求偏导
            C_de_W, C_de_b = self.backward_net(w_i, b_i)
            i += 1
            # break
            MSE_plot.append(MSE)
        plt.plot(MSE_plot)
        plt.show()
            # print(MSE) # 输出方便检查

        return MSE

    # 整合流程,执行函数
    def do_net(self):
        print(self.update_wb())



# def load_data(n=100):
#     np.random.seed(0)
#     X = np.random.randn(n) * 10
#     Z = np.tan(X)
#     # X: input, Z: target ouput
#     return X, Z

if __name__ == "__main__":   
    X, Z = load_data()
    a = BackPropagation(X, Z)
    a.do_net()

还没有测试数据来测试模型咋样,呜呜,我又没完了。

待完善

  • 添加原理介绍
  • 添加代码思想介绍
  • 添加测试集的测试
  • 添加关于对收敛性的研究(画图,原理补充,使得模型的误差收敛)

参考

使用课本:
[1] Ovidiu Calin. Deep Learning Architectures
A Mathematical Approach.Springer Nature Switzerland AG 2020.
初始化权重问题:
深度学习中神经网络的几种权重初始化方法
原理问题:
神经网络BP反向传播算法原理和详细推导流程
讲述为什么要使用激活函数,我觉着很棒!
深度学习知识点整理(二)——神经网络理解 / 反向传播 / 激活函数 / 神经网络优化

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值