反向传播推导+numpy实现

本文介绍了如何使用numpy构建一个简单的回归任务数据集,然后搭建一个三层神经网络进行前向传播计算,并详细阐述了反向传播过程中delta的计算方法以及权重和偏置的梯度计算。最后给出了代码实现,展示了一个简单的前馈神经网络(FFCN)的训练过程。
摘要由CSDN通过智能技术生成

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

数据的说明

首先,我们要做一个回归的任务,我们使用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()
神经网络误差反向传播是一种用于训练神经网络的常见方法,其基本原理如下: 1.前向传播:将输入数据通过神经网络进行正向传播,得到输出结果。 2.计算误差:将输出结果与真实标签进行比较,计算误差。 3.反向传播:将误差从输出层向输入层进行反向传播,更新每个神经元的权重和偏置,使得误差逐渐减小。 4.重复训练:重复执行前向传播、误差计算和反向传播,直到误差达到可接受的范围。 具体来说,误差反向传播的过程可以分为以下几个步骤: 1.计算输出层误差:将输出层的输出值与真实标签进行比较,计算输出层的误差。 2.计算隐藏层误差:将输出层误差按照权重进行反向传播,计算隐藏层的误差。 3.更新输出层权重和偏置:根据输出层误差和输出层的激活函数,更新输出层的权重和偏置。 4.更新隐藏层权重和偏置:根据隐藏层误差和隐藏层的激活函数,更新隐藏层的权重和偏置。 5.重复执行以上步骤:重复执行以上步骤,直到误差达到可接受的范围。 下面是一个简单的示例代码,演示了如何使用反向传播算法训练一个简单的神经网络: ```python import numpy as np # 定义激活函数 def sigmoid(x): return 1 / (1 + np.exp(-x)) # 定义神经网络 class NeuralNetwork: def __init__(self, input_size, hidden_size, output_size): self.input_size = input_size self.hidden_size = hidden_size self.output_size = output_size # 初始化权重和偏置 self.W1 = np.random.randn(self.input_size, self.hidden_size) self.b1 = np.zeros((1, self.hidden_size)) self.W2 = np.random.randn(self.hidden_size, self.output_size) self.b2 = np.zeros((1, self.output_size)) # 前向传播 def forward(self, X): self.z1 = np.dot(X, self.W1) + self.b1 self.a1 = sigmoid(self.z1) self.z2 = np.dot(self.a1, self.W2) + self.b2 self.y_hat = sigmoid(self.z2) return self.y_hat # 反向传播 def backward(self, X, y, y_hat): # 计算输出层误差 delta2 = (y_hat - y) * y_hat * (1 - y_hat) # 计算隐藏层误差 delta1 = np.dot(delta2, self.W2.T) * self.a1 * (1 - self.a1) # 更新输出层权重和偏置 dW2 = np.dot(self.a1.T, delta2) db2 = np.sum(delta2, axis=0, keepdims=True) self.W2 -= 0.1 * dW2 self.b2 -= 0.1 * db2 # 更新隐藏层权重和偏置 dW1 = np.dot(X.T, delta1) db1 = np.sum(delta1, axis=0, keepdims=True) self.W1 -= 0.1 * dW1 self.b1 -= 0.1 * db1 # 训练神经网络 def train(self, X, y): y_hat = self.forward(X) self.backward(X, y, y_hat) # 创建数据集 X = np.array([[0, 0], [0, 1], [1, 0], [1, 1]]) y = np.array([[0], [1], [1], [0]]) # 创建神经网络 nn = NeuralNetwork(2, 3, 1) # 训练神经网络 for i in range(10000): nn.train(X, y) # 测试神经网络 y_hat = nn.forward(X) print(y_hat) ```
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值