反向传播推导+numpy实现

很久没有看深度学习了,忘了好多东西。本来想着推导一下,后来发现自己不会了。
再看看以前写的代码,又避开了最终的东西,于是决定重新推导一下。

数据的说明

首先,我们要做一个回归的任务,我们使用numpy随机的生成一些数据
在这里插入图片描述
生成的代码如下,可以看到这一个二次函数,但是我们加入了一些随机的噪声使其更像真实的数据。

import numpy as np
import matplotlib.pyplot as plt
np.random.seed(333)
data = np.random.random(size=(1, 30)) * 5 # 随机生成30个点
data = np.sort(data, axis=1)
y = (data ** 2).T + np.random.randn(30, 1) * 2 # 加入随机噪声
plt.scatter(data, y.T)
plt.show()

然后我们对数据进行升维,使得我们的网络能够捕捉到更多的特征。

np.random.seed(333)
data = np.random.random(size=(1, 30)) * 5
data = np.sort(data, axis=1)
y = (data ** 2).T + np.random.randn(30, 1) * 2
t_data = np.vstack([ # 升维度
    data,
    data ** 2
])

所以我们现在每个数据有两个特征,一个是原本的x,一个是 x 2 x^2 x2。我们的数据集的形状是 2 × 30 2×30 2×30其中30是样本数,2是特征数。每一列是一个样本。

前向传播

我们首先来设定一下各个参数,如下写的参数都是其形状。

w ( 1 ) = ( 100 , 4 ) , b ( 1 ) = ( 100 , 1 ) w^{(1)}=(100, 4), b^{(1)}=(100, 1) w(1)=(100,4),b(1)=(100,1)
w ( 2 ) = ( 400 , 100 ) , b ( 2 ) = ( 400 , 1 ) w^{(2)}=(400, 100), b^{(2)}=(400, 1) w(2)=(400,100),b(2)=(400,1)
w ( 3 ) = ( 1 , 400 ) , b ( 3 ) = ( 1 , 1 ) w^{(3)}=(1, 400), b^{(3)}=(1, 1) w(3)=(1,400),b(3)=(1,1)
请添加图片描述
如上图所示,不算输入层,这是一个三层的网络,每一层左侧的连线就是权重参数和偏置参数

前向传播计算过程(这里我们只算单个样本的):

  1. z ( 1 ) = b ( 1 ) + w ( 1 ) x z^{(1)}=b^{(1)}+w^{(1)}x z(1)=b(1)+w(1)x
  2. a ( 1 ) = s i g m o i d ( z ( 1 ) ) a^{(1)}=sigmoid(z^{(1)}) a(1)=sigmoid(z(1))
  3. z ( 2 ) = b ( 2 ) + w ( 2 ) a ( 1 ) z^{(2)}=b^{(2)}+w^{(2)}a^{(1)} z(2)=b(2)+w(2)a(1)
  4. a ( 2 ) = s i g m o i d ( z ( 2 ) ) a^{(2)}=sigmoid(z^{(2)}) a(2)=sigmoid(z(2))
  5. z ( 3 ) = b ( 3 ) + w ( 3 ) a ( 2 ) z^{(3)}=b^{(3)}+w^{(3)}a^{(2)} z(3)=b(3)+w(3)a(2)
  6. L = ( z ( 3 ) − y ) 2 L = (z^{(3)} - y)^2 L=(z(3)y)2
    由于这里我们只算单个样本的误差,所以我们最后的损失函数计算不需要除以n

反向传播

delta的计算

首先,我们需要引入一个中间变量,叫做 δ j ( l ) \delta^{(l)}_j δj(l),其定义为 δ j ( l ) = ∂ L ∂ z j ( l ) \delta^{(l)}_j=\frac{\partial L}{\partial z^{(l)}_j} δj(l)=zj(l)L这是一个关键的中间变量。

  1. 第三层的 δ \delta δ计算

    由于这一层只有一个z,所以我们可以直接得出 δ j ( 3 ) = ∂ L ∂ z j ( 3 ) = 2 ( z j ( 3 ) − y ) \delta ^{(3)}_j = \frac{\partial L}{\partial z^{(3)}_j}=2(z^{(3)}_j - y) δj(3)=zj(3)L=2(zj(3)y)

  2. 第二层的 δ \delta δ计算

    我们希望递归的去计算 δ \delta δ所以,我们必须要用 δ ( 3 ) \delta^{(3)} δ(3)的计算信息来推出 δ ( 2 ) \delta^{(2)} δ(2)的,也就是我们要化成如下这种形式:
    δ j ( 2 ) = ∂ L ∂ z j ( 2 ) = ∂ L ∂ z ( 3 ) ∂ z ( 3 ) ∂ z j ( 2 ) \delta ^{(2)}_j = \frac{\partial L}{\partial z^{(2)}_j}=\frac{\partial L}{\partial z^{(3)}}\frac{\partial z^{(3)}}{\partial z^{(2)}_j} δj(2)=zj(2)L=z(3)Lzj(2)z(3)
    注意,这个式子并不是一个精确地式子,但是可以看出,通过这种链式法则的拆解,我们得到的第一项中含有 δ ( 3 ) \delta^{(3)} δ(3)
    对于 z i ( 2 ) z^{(2)}_i zi(2)它的产生,都需要经过 z j ( 3 ) z^{(3)}_j zj(3)的计算,所以根据链式求导法则我们有
    δ j ( 2 ) = ∂ L ∂ z j ( 2 ) = ∑ k = 1 1 ∂ L ∂ z k ( 3 ) ∂ z k ( 3 ) ∂ z j ( 2 ) \delta ^{(2)}_j = \frac{\partial L}{\partial z^{(2)}_j}=\sum\limits_{k=1}^1\frac{\partial L}{\partial z^{(3)}_k}\frac{\partial z^{(3)}_k}{\partial z^{(2)}_j} δj(2)=zj(2)L=k=11zk(3)Lzj(2)zk(3).
    形象的来说,就是我们利用链式求导法则计算时,我们把 ∂ L ∂ z j ( 2 ) \frac{\partial L}{\partial z^{(2)}_j} zj(2)L拆成 ∂ L ∂ z ( 3 ) ∂ z ( 3 ) ∂ z j ( 2 ) \frac{\partial L}{\partial z^{(3)}}\frac{\partial z^{(3)}}{\partial z^{(2)}_j} z(3)Lzj(2)z(3)时中间有若干个中间变量 z k ( 3 ) z^{(3)}_k zk(3), 所以我们对于每一个中间变量都需要求出它的分量。
    接下来我们求解 ∂ z k ( 3 ) ∂ z j ( 2 ) \frac{\partial z^{(3)}_k}{\partial z^{(2)}_j} zj(2)zk(3),我们写出 z ( 3 ) z^{(3)} z(3)的表达式:
    z k ( 3 ) = ∑ i w k , i ( 3 ) σ ( z i ( 2 ) ) + b k ( 3 ) z^{(3)}_k=\sum\limits_i w^{(3)}_{k,i}\sigma(z^{(2)}_i) + b^{(3)}_k zk(3)=iwk,i(3)σ(zi(2))+bk(3)
    于是我们得到 ∂ z k ( 3 ) ∂ z j ( 2 ) = w k , j σ ′ ( z j ( 2 ) ) \frac{\partial z^{(3)}_k}{\partial z^{(2)}_j}=w_{k,j}\sigma'(z_j^{(2)}) zj(2)zk(3)=wk,jσ(zj(2)), 于是我们得到最终的计算式
    δ j ( 2 ) = ∂ L ∂ z j ( 2 ) = ∑ k = 1 1 δ k ( 3 ) w k , j ( 3 ) σ ′ ( z j ( 2 ) ) \delta ^{(2)}_j = \frac{\partial L}{\partial z^{(2)}_j}=\sum\limits_{k=1}^1\delta^{(3)}_kw^{(3)}_{k,j}\sigma'(z_j^{(2)}) δj(2)=zj(2)L=k=11δk(3)wk,j(3)σ(zj(2)).
    于是我们得到 δ ( 2 ) = ( w ( 3 ) ) T δ ( 3 ) ⨂ σ ′ ( z ( 2 ) ) \delta^{(2)}=(w^{(3)})^T\delta^{(3)}\bigotimes \sigma'(z^{(2)}) δ(2)=(w(3))Tδ(3)σ(z(2))

  3. 第一层的计算

    同样的思路,我们的计算方式和第二层一模一样
    δ j ( 1 ) = ∂ L ∂ z j ( 1 ) = ∑ k = 1 400 δ k ( 2 ) w k , j ( 2 ) σ ′ ( z j ( 1 ) ) \delta ^{(1)}_j = \frac{\partial L}{\partial z^{(1)}_j}=\sum\limits_{k=1}^{400}\delta^{(2)}_kw^{(2)}_{k,j}\sigma'(z_j^{(1)}) δj(1)=zj(1)L=k=1400δk(2)wk,j(2)σ(zj(1)).
    我们写成向量形式
    [ δ 1 ( 1 ) δ 2 ( 1 ) . . . δ 100 ( 1 ) ] = [ δ 1 ( 2 ) w 1 , 1 ( 2 ) + δ 2 ( 2 ) w 2 , 1 ( 2 ) + . . . + δ 400 ( 2 ) w 400 , 1 ( 2 ) δ 1 ( 2 ) w 1 , 2 ( 2 ) + δ 2 ( 2 ) w 2 , 2 ( 2 ) + . . . + δ 400 ( 2 ) w 400 , 2 ( 2 ) . . . δ 1 ( 2 ) w 1 , 100 ( 2 ) + δ 2 ( 2 ) w 2 , 100 ( 2 ) + . . . + δ 400 ( 2 ) w 400 , 100 ( 2 ) ] ⨂ [ σ ′ ( z 1 ( 1 ) ) σ ′ ( z 2 ( 1 ) ) . . . σ ′ ( z 100 ( 1 ) ) ] \begin{bmatrix} \delta^{(1)}_1 \\ \delta^{(1)}_2 \\ ...\\ \delta^{(1)}_{100} \end{bmatrix} = \begin{bmatrix} \delta^{(2)}_1w^{(2)}_{1,1}+\delta^{(2)}_2w^{(2)}_{2,1}+...+\delta^{(2)}_{400}w^{(2)}_{400,1}\\ \delta^{(2)}_1w^{(2)}_{1,2}+\delta^{(2)}_2w^{(2)}_{2,2}+...+\delta^{(2)}_{400}w^{(2)}_{400,2}\\ ...\\ \delta^{(2)}_1w^{(2)}_{1,100}+\delta^{(2)}_2w^{(2)}_{2,100}+...+\delta^{(2)}_{400}w^{(2)}_{400,100} \end{bmatrix} \bigotimes \begin{bmatrix} \sigma'(z^{(1)}_1)\\ \sigma'(z^{(1)}_2)\\ ...\\ \sigma'(z^{(1)}_{100}) \end{bmatrix} δ1(1)δ2(1)...δ100(1) = δ1(2)w1,1(2)+δ2(2)w2,1(2)+...+δ400(2)w400,1(2)δ1(2)w1,2(2)+δ2(2)w2,2(2)+...+δ400(2)w400,2(2)...δ1(2)w1,100(2)+δ2(2)w2,100(2)+...+δ400(2)w400,100(2) σ(z1(1))σ(z2(1))...σ(z100(1))
    可以得到 δ ( 1 ) = ( w ( 2 ) ) T δ ( 2 ) ⨂ σ ′ ( z ( 1 ) ) \delta^{(1)}=(w^{(2)})^T\delta^{(2)}\bigotimes \sigma'(z^{(1)}) δ(1)=(w(2))Tδ(2)σ(z(1))

b和w的偏导计算

w w w b b b的梯度计算

有了 δ \delta δ之后,想要计算这两个,可以说是很简单了。
由于 ∂ L ∂ b i = δ i ∂ z i ∂ b i \frac{\partial L}{\partial b_i}=\delta_i \frac{\partial z_i}{\partial b_i} biL=δibizi根据z和b的关系,所以 ∂ z i ∂ b i = 1 \frac{\partial z_i}{\partial b_i}=1 bizi=1最终我们得到了梯度
∂ L ∂ b i = δ i \frac{\partial L}{\partial b_i}=\delta_i biL=δi
接下来我们计算 w w w
∂ L ∂ w i , j = δ i ∂ z i ∂ w i , j = δ i a j \frac{\partial L}{\partial w_{i,j}}=\delta_i \frac{\partial z_i}{\partial w_{i,j}}=\delta_i a_j wi,jL=δiwi,jzi=δiaj然后我们改写成矩阵形式
∂ L ∂ w ( l ) = δ ( l ) ( a ( l − 1 ) ) T \frac{\partial L}{\partial w^{(l)}}=\delta^{(l)}(a^{(l-1)})^T w(l)L=δ(l)(a(l1))T
总结一下
∂ L ∂ b ( l ) = δ ( l ) ∂ L ∂ w ( l ) = δ ( l ) ( a ( l − 1 ) ) T \frac{\partial L}{\partial b^{(l)}}=\delta^{(l)}\\\frac{\partial L}{\partial w^{(l)}}=\delta^{(l)}(a^{(l-1)})^T b(l)L=δ(l)w(l)L=δ(l)(a(l1))T

代码实现

class FFCN:
    
    def __init__(self) -> None:
        self.w1 = np.random.randn(100, 2)
        self.b1 = np.random.randn(100).reshape(-1, 1)
        self.w2 = np.random.randn(400, 100)
        self.b2 = np.random.randn(400).reshape(-1, 1)
        self.w3 = np.random.randn(1, 400)
        self.b3 = np.random.randn(1).reshape(-1, 1)

    def forward(self, x: np.ndarray):
        """
        x: (4, 1)
        """
        self.z1 = self.w1.dot(x) + self.b1
        self.a1 = self.__sigmoid(self.z1)

        self.z2 = self.w2.dot(self.a1) + self.b2
        self.a2 = self.__sigmoid(self.z2)

        self.z3 = self.w3.dot(self.a2) + self.b3
        return self.z3
    
    def predict(self, x: np.ndarray): # 用于预测多组的结果
        return np.array([self.forward(x[:, i].reshape(-1, 1))[0][0] for i in range(x.shape[1])])
    
    def backpp(self, data: np.ndarray, target: np.ndarray, lr:float=0.002, iter:int=200):
    """
    lr: 学习率
    iter: 迭代次数
    """
        for _ in range(iter):
            loss = 0
            for i in range(data.shape[1]):
                x = data[:, i].reshape(-1, 1)
                y = target[i]
                loss += (self.forward(x) - y) ** 2

                self.delta3 = 2 * (self.z3 - y)
                self.delta2 = self.w3.T.dot(self.delta3) * self.__dsigmoid(self.z2)
                self.delta1 = self.w2.T.dot(self.delta2) * self.__dsigmoid(self.z1)

                self.dw1 = self.delta1.dot(x.reshape(1,-1))
                self.dw2 = self.delta2.dot(self.a1.T)
                self.dw3 = self.delta3.dot(self.a2.T)
                self.__step(lr)
            print("loss of", _, ":", loss[0][0])
            loss = 0

    def __sigmoid(self, x):# 激活函数
        return 1 / (1 + np.exp(-x))
    
    def __dsigmoid(self, x):
        return np.exp(x) / ((1 + np.exp(x)) ** 2)

    def __step(self, lr: float): # 更新参数权重
        self.w1 -= self.dw1 * lr
        self.w2 -= self.dw2 * lr
        self.w3 -= self.dw3 * lr
        self.b1 -= self.delta1 * lr
        self.b2 -= self.delta2 * lr
        self.b3 -= self.delta3 * lr

效果:
在这里插入图片描述
完整代码:

import numpy as np
import matplotlib.pyplot as plt

"""
造一个三层的神经网络

"""

class FFCN:
    
    def __init__(self) -> None:
        self.w1 = np.random.randn(100, 2)
        self.b1 = np.random.randn(100).reshape(-1, 1)
        self.w2 = np.random.randn(400, 100)
        self.b2 = np.random.randn(400).reshape(-1, 1)
        self.w3 = np.random.randn(1, 400)
        self.b3 = np.random.randn(1).reshape(-1, 1)

    def forward(self, x: np.ndarray):
        """
        x: (4, 1)
        """
        self.z1 = self.w1.dot(x) + self.b1
        self.a1 = self.__sigmoid(self.z1)

        self.z2 = self.w2.dot(self.a1) + self.b2
        self.a2 = self.__sigmoid(self.z2)

        self.z3 = self.w3.dot(self.a2) + self.b3
        return self.z3
    
    def predict(self, x: np.ndarray):
        return np.array([self.forward(x[:, i].reshape(-1, 1))[0][0] for i in range(x.shape[1])])
    
    def backpp(self, data: np.ndarray, target: np.ndarray, lr:float=0.002, iter:int=200):
        for _ in range(iter):
            loss = 0
            for i in range(data.shape[1]):
                x = data[:, i].reshape(-1, 1)
                y = target[i]
                loss += (self.forward(x) - y) ** 2

                self.delat3 = 2 * (self.z3 - y)
                self.delat2 = self.w3.T.dot(self.delat3) * self.__dsigmoid(self.z2)
                self.delta1 = self.w2.T.dot(self.delat2) * self.__dsigmoid(self.z1)

                self.dw1 = self.delta1.dot(x.reshape(1,-1))
                self.dw2 = self.delat2.dot(self.a1.T)
                self.dw3 = self.delat3.dot(self.a2.T)
                self.__step(lr)
            print(_, ":", loss[0][0])
            loss = 0

    def __sigmoid(self, x):
        return 1 / (1 + np.exp(-x))
    
    def __dsigmoid(self, x):
        return np.exp(x) / ((1 + np.exp(x)) ** 2)

    def __step(self, lr: float):
        self.w1 -= self.dw1 * lr
        self.w2 -= self.dw2 * lr
        self.w3 -= self.dw3 * lr
        self.b1 -= self.delta1 * lr
        self.b2 -= self.delat2 * lr
        self.b3 -= self.delat3 * lr

model = FFCN()

np.random.seed(111)
data = np.random.random(size=(1, 30)) * 5
data = np.sort(data, axis=1)
y = (data ** 2).T + np.random.randn(30, 1) * 2
t_data = np.vstack([
    data,
    data ** 2
])
model.backpp(t_data, y)
plt.scatter(data, y.T)
plt.plot(data.squeeze(0), model.predict(t_data), color='r')
plt.show()
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值