反向传播推导+numpy实现

news/2024/11/15 1:34:39/

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

数据的说明

首先,我们要做一个回归的任务,我们使用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.b1self.a1 = self.__sigmoid(self.z1)self.z2 = self.w2.dot(self.a1) + self.b2self.a2 = self.__sigmoid(self.z2)self.z3 = self.w3.dot(self.a2) + self.b3return self.z3def 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 = 0for i in range(data.shape[1]):x = data[:, i].reshape(-1, 1)y = target[i]loss += (self.forward(x) - y) ** 2self.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 = 0def __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 * lrself.w2 -= self.dw2 * lrself.w3 -= self.dw3 * lrself.b1 -= self.delta1 * lrself.b2 -= self.delta2 * lrself.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.b1self.a1 = self.__sigmoid(self.z1)self.z2 = self.w2.dot(self.a1) + self.b2self.a2 = self.__sigmoid(self.z2)self.z3 = self.w3.dot(self.a2) + self.b3return self.z3def 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 = 0for i in range(data.shape[1]):x = data[:, i].reshape(-1, 1)y = target[i]loss += (self.forward(x) - y) ** 2self.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 = 0def __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 * lrself.w2 -= self.dw2 * lrself.w3 -= self.dw3 * lrself.b1 -= self.delta1 * lrself.b2 -= self.delat2 * lrself.b3 -= self.delat3 * lrmodel = 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()

http://www.ppmy.cn/news/49447.html

相关文章

《Java8实战》第12章 新的日期和时间 API

原来的Java的时间类Date、java.util.Calendar类都不太好,以语言无关方式格式化和解析日期或时间的 DateFormat 方法也有线程安全的问题 12.1 LocalDate、LocalTime、LocalDateTime、Instant、Duration 以及 Period 12.1.1 使用 LocalDate 和 LocalTime LocalDate…

c++学习之类与对象3

目录 成员变量和函数的存储 this指针 this指针的工作原理 this指针的应用 const修饰的成员函数 友元 友元的语法 1.普通全局函数成为类的友元 2.类的某个成员函数作为另一个类的友元 整个类作为另一个类的友元 运算符重载 1 运算符重载的基本概念 2 重载加号运算符…

Medical X-rays Dataset汇总(长期更新)

目录​​​​​​​ ChestX-ray8 ChestX-ray14 VinDr-CXR VinDr-PCXR ChestX-ray8 ChestX-ray8 is a medical imaging dataset which comprises 108,948 frontal-view X-ray images of 32,717 (collected from the year of 1992 to 2015) unique patients with the text-mi…

FPGA基于XDMA实现PCIE X8采集AD7606数据 提供工程源码和QT上位机程序和技术支持

1、前言 PCIE(PCI Express)采用了目前业内流行的点对点串行连接,比起 PCI 以及更早期的计算机总线的共享并行架构,每个设备都有自己的专用连接,不需要向整个总线请求带宽,而且可以把数据传输率提高到一个很…

CSS浮动

CSS结构伪类选择器 结构伪类选择器用于选择文档结构中特定位置的元素,根据元素在其父元素中的相对位置或兄弟关系进行选择。这些选择器使得你能够更灵活地控制页面的样式。 :first-child(常用) 用途:选择元素的第一个子元素。 …

六:内存回收

内存回收: 应用程序通过 malloc 函数申请内存的时候,实际上申请的是虚拟内存,此时并不会分配物理内存。 当应用程序读写了这块虚拟内存,CPU 就会去访问这个虚拟内存, 这时会发现这个虚拟内存没有映射到物理内存&…

【PlumGPT】与PlumGPT开启智能对话之旅

文章目录 一、前言二、PlumGPT介绍篇三、PlumGPT登录篇四、PlumGPT体验篇1、与PlumGPT聊天2、让PlumGPT翻译3、让PlumGPT创作4、请PlumGPT写推荐信5、让PlumGPT展示图片6、让PlumGPT充当百科小助手 五、PlumGPT总结篇 PlumGPT入口体验链接:https://plumgpt.com 一、…

Android 10.0 user模式下解除系统进入recovery功能的限制

1.前言 在10.0的系统rom定制化开发中,系统中recovery模式功能也是很重要的一部分,而在原生系统中,对于debug模式的产品,可以通过电源键和音量+键进入recovery模式, 但是在user模式下的产品,对于通过这种方式,进入recovery模式就受限制了,防止用户无操作为了产品安全等…