一文搞定BP神经网络——从原理到应用(原理篇)
文章目录
- 0. 什么是人工神经网络?
- 1. 神经网络初探
- 1.1 神经元模型
- 1.2 神经元激活函数
- 1.3 神经网络结构
- 2. 损失函数和代价函数
- 3. 反向传播
- 3.1 矩阵补充知识
- **3.1.1 求梯度矩阵**
- **3.1.2 海塞矩阵**
- **3.1.3 总结**
- 3.3 梯度下降法原理
- 3.4 反向传播原理(四个基础等式)
- 3.5 反向传播总结
- **3.5.1 单样本输入公式表**
- **3.5.2 多样本输入公式表**
- **3.5.3 关于超参数**
- 4. 是不是猫?
- 4.1 辅助函数
- 4.2 前向传播过程
- 4.3 反向传播过程
- 4.4 测试结果
- 5. 本文小结
- 订正与答疑:
- 1. 具体解释一下公式1里面的“堆砌”是什么意思?
- 2. 公式2写成矩阵形式为什么系数矩阵会有转置?自己没搞懂。
- 3. 公式3能具体讲一下矩阵形式是怎么来的吗?
- 4. 为什么会损失函数不用最小二乘法?
- 5. 能不能顺便介绍一下Python环境的管理?
- 6. 为什么w的初始化使用随机初始化,而b参数的初始化全部初始化为0?
本文着重讲述经典BP神经网络的数学推导过程,并辅助一个小例子。本文不会介绍机器学习库(比如sklearn, TensorFlow等)的使用。 欲了解卷积神经网络的内容,请参见我的另一篇博客一文搞定卷积神经网络——从原理到应用。
本文难免会有叙述不合理的地方,希望读者可以在评论区反馈。我会及时吸纳大家的意见,并在之后的chat里进行说明。
本文参考了一些资料,在此一并列出。
- http://neuralnetworksanddeeplearning.com/chap2.html
- https://www.deeplearning.ai/ coursera对应课程视频讲义
- coursera华盛顿大学机器学习专项
- 周志华《机器学习》
- 李航《统计学习方法》
- 张明淳《工程矩阵理论》
0. 什么是人工神经网络?
首先给出一个经典的定义:“神经网络是由具有适应性的简单单元组成的广泛并行互连网络,它的组织能够模拟生物神经系统对真实世界物体所作出的交互反应”[Kohonen, 1988]。
这种说法虽然很经典,但是对于初学者并不是很友好。比如我在刚开始学习的时候就把人工神经网络想象地很高端,以至于很长一段时间都不能理解为什么神经网络能够起作用。类比最小二乘法线性回归问题,在求解数据拟合直线的时候,我们是采用某种方法让预测值和实际值的“偏差”尽可能小。同理,BP神经网络也做了类似的事情——即通过让“偏差”尽可能小,使得神经网络模型尽可能好地拟合数据集。
1. 神经网络初探
1.1 神经元模型
神经元模型是模拟生物神经元结构而被设计出来的。典型的神经元结构如下图1所示:
【图1 典型神经元结构 (图片来自维基百科)】
神经元大致可以分为树突、突触、细胞体和轴突。树突为神经元的输入通道,其功能是将其它神经元的动作电位传递至细胞体。其它神经元的动作电位借由位于树突分支上的多个突触传递至树突上。神经细胞可以视为有两种状态的机器,激活时为“是”,不激活时为“否”。神经细胞的状态取决于从其他神经细胞接收到的信号量,以及突触的性质(抑制或加强)。当信号量超过某个阈值时,细胞体就会被激活,产生电脉冲。电脉冲沿着轴突并通过突触传递到其它神经元。(内容来自维基百科“感知机”)
同理,我们的神经元模型就是为了模拟上述过程,典型的神经元模型如下:
【图2 典型神经元模型结构 (摘自周志华老师《机器学习》第97页)】
这个模型中,每个神经元都接受来自其它神经元的输入信号,每个信号都通过一个带有权重的连接传递,神经元把这些信号加起来得到一个总输入值,然后将总输入值与神经元的阈值进行对比(模拟阈值电位),然后通过一个“激活函数”处理得到最终的输出(模拟细胞的激活),这个输出又会作为之后神经元的输入一层一层传递下去。
1.2 神经元激活函数
本文主要介绍2种激活函数,分别是sigmoidsigmoidsigmoid和relurelurelu函数,函数公式如下:
sigmoid(z)=11+e−zsigmoid(z)=\frac{1}{1+e^{-z}}sigmoid(z)=1+e−z1
relu(z)={zz>00z≤0relu(z)= \left\{ \begin{array}{rcl} z & z>0\\ 0&z\leq0\end{array} \right.relu(z)={z0z>0z≤0
做函数图如下:
sigmoid(z)sigmoid(z)sigmoid(z)
relu(z)relu(z)relu(z)
【图3 激活函数】
补充说明
【补充说明的内容建议在看完后文的反向传播部分之后再回来阅读,我只是为了文章结构的统一把这部分内容添加在了这里】引入激活函数的目的是在模型中引入非线性。如果没有激活函数,那么无论你的神经网络有多少层,最终都是一个线性映射,单纯的线性映射无法解决线性不可分问题。引入非线性可以让模型解决线性不可分问题。
一般来说,在神经网络的中间层更加建议使用relurelurelu函数,两个原因:
- relurelurelu函数计算简单,可以加快模型速度;
- 由于反向传播过程中需要计算偏导数,通过求导可以得到sigmoidsigmoidsigmoid函数导数的最大值为0.25,如果使用sigmoidsigmoidsigmoid函数的话,每一层的反向传播都会使梯度最少变为原来的四分之一,当层数比较多的时候可能会造成梯度消失,从而模型无法收敛。
1.3 神经网络结构
我们使用如下神经网络结构来进行介绍,第0层是输入层(3个神经元), 第1层是隐含层(2个神经元),第2层是输出层:
【图4 神经网络结构(手绘)】
我们使用以下符号约定,wjk[l]w_{jk}^{[l]}wjk[l]表示从网络第(l−1)th(l-1)^{th}(l−1)th中kthk^{th}kth个神经元指向第lthl^{th}lth中第jthj^{th}jth个神经元的连接权重,比如上图中w21[1]w^{[1]}_{21}w21[1]即从第0层第1个神经元指向第1层第2个神经元的权重。同理,我们使用bj[l]b^{[l]}_jbj[l]来表示第lthl^{th}lth层中第jthj^{th}jth神经元的偏差,用zj[l]z^{[l]}_jzj[l]来表示第lthl^{th}lth层中第jthj^{th}jth神经元的线性结果,用aj[l]a^{[l]}_jaj[l]来表示第lthl^{th}lth层中第jthj^{th}jth神经元的激活函数输出。
激活函数使用符号σ\sigmaσ表示,因此,第lthl^{th}lth层中第jthj^{th}jth神经元的激活为:
aj[l]=σ(∑kwjk[l]ak[l−1]+bj[l])a^{[l]}_j=\sigma(\sum_kw^{[l]}_{jk}a^{[l-1]}_k+b^{[l]}_j)aj[l]=σ(k∑wjk[l]ak[l−1]+bj[l])
现在,我们使用矩阵形式重写这个公式:
定义w[l]w^{[l]}w[l]表示权重矩阵,它的每一个元素表示一个权重,即每一行都是连接第lll层的权重,用上图举个例子就是:
w[1]=[w11[1]w12[1]w13[1]w21[1]w22[1]w23[1]]w^{[1]}=\left[ \begin{array}{cc} w_{11}^{[1]} & w_{12}^{[1]} & w_{13}^{[1]} \\ w_{21}^{[1]}& w_{22}^{[1]} & w_{23}^{[1]}\end{array}\right]w[1]=[w11[1]w21[1]w12[1]w22[1]w13[1]w23[1]]
同理,
b[1]=[b1[1]b2[1]]b^{[1]}=\left[ \begin{array}{cc}b^{[1]}_1 \\ b^{[1]}_2 \end{array}\right]b[1]=[b1[1]b2[1]]
z[1]=[w11[1]w12[1]w13[1]w21[1]w22[1]w23[1]]⋅[a1[0]a2[0]a3[0]]+[b1[1]b2[1]]=[w11[1]a1[0]+w12[1]a2[0]+w13[1]a3[0]+b1[1]w21[1]a1[0]+w22[1]a2[0]+w23[1]a3[0]+b2[1]]z^{[1]}=\left[ \begin{array}{cc} w_{11}^{[1]} & w_{12}^{[1]} & w_{13}^{[1]} \\ w_{21}^{[1]}& w_{22}^{[1]} & w_{23}^{[1]}\end{array}\right]\cdot \left[ \begin{array}{cc} a^{[0]}_1 \\ a^{[0]}_2 \\ a^{[0]}_3 \end{array}\right] +\left[ \begin{array}{cc}b^{[1]}_1 \\ b^{[1]}_2 \end{array}\right]=\left[ \begin{array}{cc} w_{11}^{[1]}a^{[0]}_1+w_{12}^{[1]}a^{[0]}_2+w_{13}^{[1]}a^{[0]}_3+b^{[1]}_1 \\ w^{[1]}_{21}a^{[0]}_1+w_{22}^{[1]}a^{[0]}_2+w_{23}^{[1]}a^{[0]}_3+b^{[1]}_2\end{array}\right]z[1]=[w11[1]w21[1]w12[1]w22[1]w13[1]w23[1]]⋅⎣⎢⎡a1[0]a2[0]a3[0]⎦⎥⎤+[b1[1]b2[1]]=[w11[1]a1[0]+w12[1]a2[0]+w13[1]a3[0]+b1[1]w21[1]a1[0]+w22[1]a2[0]+w23[1]a3[0]+b2[1]]
更一般地,我们可以把前向传播过程表示:
a[l]=σ(w[l]a[l−1]+b[l])a^{[l]}=\sigma(w^{[l]}a^{[l-1]}+b^{[l]})a[l]=σ(w[l]a[l−1]+b[l])
到这里,我们已经讲完了前向传播的过程,值得注意的是,这里我们只有一个输入样本,对于多个样本同时输入的情况是一样的,只不过我们的输入向量不再是一列,而是m列,每一个都表示一个输入样本。
多样本输入情况下的表示为:
Z[l]=w[l]⋅A[l−1]+b[l]Z^{[l]}=w^{[l]}\cdot A^{[l-1]}+b^{[l]}Z[l]=w[l]⋅A[l−1]+b[l]
A[l]=σ(Z[l])A^{[l]}=\sigma(Z^{[l]})A[l]=σ(Z[l])
其中,此时A[l−1]=[∣∣…∣a[l−1](1)a[l−1](2)…a[l−1](m)∣∣…∣]A^{[l-1]}=\left[ \begin{array}{cc} |&|&\ldots&| \\a^{[l-1](1)}&a^{[l-1](2)}&\ldots&a^{[l-1](m)} \\ |&|&\ldots&|\end{array}\right]A[l−1]=⎣⎡∣a[l−1](1)∣∣a[l−1](2)∣………∣a[l−1](m)∣⎦⎤
每一列都表示一个样本,从样本1到m
w[l]w^{[l]}w[l]的含义和原来完全一样,Z[l]Z^{[l]}Z[l]也会变成m列,每一列表示一个样本的计算结果。
之后我们的叙述都是先讨论单个样本的情况,再扩展到多个样本同时计算。
2. 损失函数和代价函数
说实话,**损失函数(Loss Function)和代价函数(Cost Function)**并没有一个公认的区分标准,很多论文和教材似乎把二者当成了差不多的东西。
为了后面描述的方便,我们把二者稍微做一下区分(这里的区分仅仅对本文适用,对于其它的文章或教程需要根据上下文自行判断含义):
损失函数主要指的是对于单个样本的损失或误差;代价函数表示多样本同时输入模型的时候总体的误差——每个样本误差的和然后取平均值。
举个例子,如果我们把单个样本的损失函数定义为:
L(a,y)=−[y⋅log(a)+(1−y)⋅log(1−a)]L(a,y)=-[y \cdot log(a)+(1-y)\cdot log(1-a)]L(a,y)=−[y⋅log(a)+(1−y)⋅log(1−a)]
那么对于m个样本,代价函数则是:
C=−1m∑i=0m(y(i)⋅log(a(i))+(1−y(i))⋅log(1−a(i)))C=-\frac{1}{m}\sum_{i=0}^m(y^{(i)}\cdot log(a^{(i)})+(1-y^{(i)})\cdot log(1-a^{(i)}))C=−m1i=0∑m(y(i)⋅log(a(i))+(1−y(i))⋅log(1−a(i)))
3. 反向传播
反向传播的基本思想就是通过计算输出层与期望值之间的误差来调整网络参数,从而使得误差变小。
反向传播的思想很简单,然而人们认识到它的重要作用却经过了很长的时间。后向传播算法产生于1970年,但它的重要性一直到David Rumelhart,Geoffrey Hinton和Ronald Williams于1986年合著的论文发表才被重视。
事实上,人工神经网络的强大力量几乎就是建立在反向传播算法基础之上的。反向传播基于四个基础等式,数学是优美的,仅仅四个等式就可以概括神经网络的反向传播过程,然而理解这种优美可能需要付出一些脑力。事实上,反向传播如此之难,以至于相当一部分初学者很难进行独立推导。所以如果读者是初学者,希望读者可以耐心地研读本节。对于初学者,我觉得拿出1-3个小时来学习本小节是比较合适的,当然,对于熟练掌握反向传播原理的读者,你可以在十几分钟甚至几分钟之内快速浏览本节的内容。
3.1 矩阵补充知识
对于大部分理工科的研究生,以及学习过矩阵论或者工程矩阵理论相关课程的读者来说,可以跳过本节。
本节主要面向只学习过本科线性代数课程或者已经忘记矩阵论有关知识的读者。
总之,具备了本科线性代数知识的读者阅读这一小节应该不会有太大问题。本节主要在线性代数的基础上做一些扩展。(不排除少数本科线性代数课程也涉及到这些内容,如果感觉讲的简单的话,勿喷)
3.1.1 求梯度矩阵
假设函数 f:Rm×n→Rf:R^{m\times n}\rightarrow Rf:Rm×n→R可以把输入矩阵(shape: m×nm\times nm×n)映射为一个实数。那么,函数fff的梯度定义为:
∇Af(A)=[∂f(A)∂A11∂f(A)∂A12…∂f(A)∂A1n∂f(A)∂A21∂f(A)∂A22…∂f(A)∂A2n⋮⋮⋱⋮∂f(A)∂Am1∂f(A)∂Am2…∂f(A)∂Amn]\nabla_Af(A)=\left[ \begin{array}{cc} \frac{\partial f(A)}{\partial A_{11}}&\frac{\partial f(A)}{\partial A_{12}}&\ldots&\frac{\partial f(A)}{\partial A_{1n}} \\ \frac{\partial f(A)}{\partial A_{21}}&\frac{\partial f(A)}{\partial A_{22}}&\ldots&\frac{\partial f(A)}{\partial A_{2n}} \\\vdots &\vdots &\ddots&\vdots\\ \frac{\partial f(A)}{\partial A_{m1}}&\frac{\partial f(A)}{\partial A_{m2}}&\ldots&\frac{\partial f(A)}{\partial A_{mn}}\end{array}\right]∇Af(A)=⎣⎢⎢⎢⎢⎡∂A11∂f(A)∂A21∂f(A)⋮∂Am1∂f(A)∂A12∂f(A)∂A22∂f(A)⋮∂Am2∂f(A)……⋱…∂A1n∂f(A)∂A2n∂f(A)⋮∂Amn∂f(A)⎦⎥⎥⎥⎥⎤
即(∇Af(A))ij=∂f(A)∂Aij(\nabla_Af(A))_{ij}=\frac{\partial f(A)}{\partial A_{ij}}(∇Af(A))ij=∂Aij∂f(A)
同理,一个输入是向量(向量一般指列向量,本文在没有特殊声明的情况下默认指的是列向量)的函数f:Rn×1→Rf:R^{n\times 1}\rightarrow Rf:Rn×1→R,则有:
∇xf(x)=[∂f(x)∂x1∂f(x)∂x2⋮∂f(x)∂xn]\nabla_xf(x)=\left[ \begin{array}{cc}\frac{\partial f(x)}{\partial x_1}\\ \frac{\partial f(x)}{\partial x_2}\\ \vdots \\ \frac{\partial f(x)}{\partial x_n} \end{array}\right]∇xf(x)=⎣⎢⎢⎢⎢⎡∂x1∂f(x)∂x2∂f(x)⋮∂xn∂f(x)⎦⎥⎥⎥⎥⎤
注意:这里涉及到的梯度求解的前提是函数fff 返回的是一个实数,如果函数返回的是一个矩阵或者向量,那么我们是没有办法求梯度的。比如,对函数f(A)=∑i=0m∑j=0nAij2f(A)=\sum_{i=0}^m\sum_{j=0}^nA_{ij}^2f(A)=∑i=0m∑j=0nAij2,由于返回一个实数,我们可以求解梯度矩阵。如果f(x)=Ax(A∈Rm×n,x∈Rn×1)f(x)=Ax (A\in R^{m\times n}, x\in R^{n\times 1})f(x)=Ax(A∈Rm×n,x∈Rn×1),由于函数返回一个mmm行1列的向量,因此不能对fff求梯度矩阵。
根据定义,很容易得到以下性质:
∇x(f(x)+g(x))=∇xf(x)+∇xg(x)\nabla_x(f(x)+g(x))=\nabla_xf(x)+\nabla_xg(x)∇x(f(x)+g(x))=∇xf(x)+∇xg(x)
∇(tf(x))=t∇f(x),t∈R\nabla(tf(x))=t\nabla f(x), t\in R∇(tf(x))=t∇f(x),t∈R
有了上述知识,我们来举个例子:
定义函数f:Rm→R,f(z)=zTzf:R^m\rightarrow R, f(z)=z^Tzf:Rm→R,f(z)=zTz,那么很容易得到∇zf(z)=2z\nabla_zf(z)=2z∇zf(z)=2z,具体请读者自己证明。
3.1.2 海塞矩阵
定义一个输入为nnn维向量,输出为实数的函数f:Rn→Rf:R^n\rightarrow Rf:Rn→R,那么海塞矩阵(Hessian Matrix)定义为多元函数fff的二阶偏导数构成的方阵:
∇x2f(x)=[∂2f(x)∂x12∂2f(x)∂x1∂x2…∂2f(x)∂x1∂xn∂2f(x)∂x2∂x1∂2f(x)∂x22…∂2f(x)∂x2∂xn⋮⋮⋱⋮∂2f(x)∂xn∂x1∂2f(x)∂xn∂x2…∂2f(x)∂xn2]\nabla^2_xf(x)=\left[ \begin{array}{cc} \frac{\partial^2f(x)}{\partial x_1^2}&\frac{\partial^2f(x)}{\partial x_1\partial x_2}&\ldots &\frac{\partial^2f(x)}{\partial x_1\partial x_n}\\ \frac{\partial^2f(x)}{\partial x_2\partial x_1}&\frac{\partial^2f(x)}{\partial x_2^2}&\ldots&\frac{\partial^2f(x)}{\partial x_2\partial x_n}\\ \vdots&\vdots&\ddots&\vdots\\\frac{\partial^2f(x)}{\partial x_n\partial x_1}&\frac{\partial^2f(x)}{\partial x_n\partial x_2}&\ldots&\frac{\partial^2f(x)}{\partial x_n^2}\end{array}\right]∇x2f(x)=⎣⎢⎢⎢⎢⎢⎡∂x12∂2f(x)∂x2∂x1∂2f(x)⋮∂xn∂x1∂2f(x)∂x1∂x2∂2f(x)∂x22∂2f(x)⋮∂xn∂x2∂2f(x)……⋱…∂x1∂xn∂2f(x)∂x2∂xn∂2f(x)⋮∂xn2∂2f(x)⎦⎥⎥⎥⎥⎥⎤
由上式可以看出,海塞矩阵总是对称阵。
注意:很多人把海塞矩阵看成∇xf(x)\nabla _xf(x)∇xf(x)的导数,这是不对的。只能说,海塞矩阵的每个元素都是函数fff二阶偏导数。那么,有什么区别呢?
首先,来看正确的解释。**海塞矩阵的每个元素是函数fff的二阶偏导数。**拿∂2f(x)∂x1∂x2\frac{\partial^2f(x)}{\partial x_1\partial x_2}∂x1∂x2∂2f(x)举个例子,函数fff对x1x_1x1求偏导得到的是一个实数,比如∂2f(x)∂x1=x23x1\frac{\partial^2f(x)}{\partial x_1}=x_2^3x_1∂x1∂2f(x)=x23x1,因此继续求偏导是有意义的,继续对x2x_2x2求偏导可以得到3x1x223x_1x_2^23x1x22。
然后,来看一下错误的理解。把海塞矩阵看成∇xf(x)\nabla _xf(x)∇xf(x)的导数,也就是说错误地以为∇x2f(x)=∇x(∇xf(x))\nabla^2_xf(x)=\nabla_x(\nabla_xf(x))∇x2f(x)=∇x(∇xf(x)),要知道,∇xf(x)\nabla_xf(x)∇xf(x)是一个向量,而在上一小节我们已经重点强调过,在我们的定义里对向量求偏导是没有定义的。
但是∇x∂f(x)∂xi\nabla_x\frac{\partial f(x)}{\partial x_i}∇x∂xi∂f(x)是有意义的,因为∂f(x)∂xi\frac{\partial f(x)}{\partial x_i}∂xi∂f(x)是一个实数,具体地:
∇x∂f(x)∂xi=[∂2f(x)∂xi∂x1∂2f(x)∂xi∂x2⋮∂2f(x)∂xi∂xn]\nabla_x\frac{\partial f(x)}{\partial x_i}=\left[ \begin{array}{cc} \frac{\partial^2f(x)}{\partial x_i\partial x_1}\\\frac{\partial^2f(x)}{\partial x_i\partial x_2}\\\vdots\\\frac{\partial^2f(x)}{\partial x_i\partial x_n}\end{array}\right]∇x∂xi∂f(x)=⎣⎢⎢⎢⎢⎢⎡∂xi∂x1∂2f(x)∂xi∂x2∂2f(x)⋮∂xi∂xn∂2f(x)⎦⎥⎥⎥⎥⎥⎤
即海塞矩阵的第i行(或列)。
希望读者可以好好区分。
3.1.3 总结
根据3.1.1和3.1.2小节的内容很容易得到以下等式:
b∈Rn,x∈Rn,A∈Rn×n并且A是对称矩阵b\in R^{n}, x\in R^n, A\in R^{n\times n}并且A 是对称矩阵b∈Rn,x∈Rn,A∈Rn×n并且A是对称矩阵
b,xb,xb,x均为列向量
那么,
∇xbTx=b\nabla_xb^Tx=b∇xbTx=b
∇xxTAx=2Ax(A是对称阵)\nabla_xx^TAx=2Ax(A是对称阵)∇xxTAx=2Ax(A是对称阵)
∇x2xTAx=2A(A是对称阵)\nabla^2_xx^TAx=2A(A是对称阵)∇x2xTAx=2A(A是对称阵)这些公式可以根据前述定义自行推导,有兴趣的读者可以自己推导一下。
####3.2 矩阵乘积和对应元素相乘
在下一节讲解反向传播原理的时候,尤其是把公式以矩阵形式表示的时候,需要大家时刻区分什么时候需要矩阵相乘,什么时候需要对应元素相乘。
比如对于矩阵A=[1234],矩阵B=[−1−2−3−4]A=\left[ \begin{array}{cc} 1&2\\3&4\end{array}\right],矩阵B=\left[ \begin{array}{cc} -1&-2\\-3&-4\end{array}\right]A=[1324],矩阵B=[−1−3−2−4]
矩阵相乘
AB=[1×−1+2×−31×−2+2×−43×−1+4×−33×−2+4×−4]=[−7−10−15−22]AB=\left[\begin{array}{cc}1\times -1+2\times -3&1\times -2+2\times -4\\3\times -1+4\times -3&3\times -2+4\times -4\end{array}\right]=\left[\begin{array}{cc}-7&-10\\-15&-22\end{array}\right]AB=[1×−1+2×−33×−1+4×−31×−2+2×−43×−2+4×−4]=[−7−15−10−22]
对应元素相乘使用符号⊙\odot⊙表示:
A⊙B=[1×−12×−23×−34×−4]=[−1−4−9−16]A\odot B=\left[\begin{array}{cc}1\times -1&2\times -2 \\ 3\times -3&4\times -4\end{array}\right]=\left[\begin{array}{cc}-1&-4 \\ -9&-16\end{array}\right]A⊙B=[1×−13×−32×−24×−4]=[−1−9−4−16]
3.3 梯度下降法原理
通过之前的介绍,相信大家都可以自己求解梯度矩阵(向量)了。
那么梯度矩阵(向量)求出来的意义是什么?从几何意义讲,梯度矩阵代表了函数增加最快的方向,因此,沿着与之相反的方向就可以更快找到最小值。如图5所示:
【图5 梯度下降法 图片来自百度】
反向传播的过程就是利用梯度下降法原理,慢慢的找到代价函数的最小值,从而得到最终的模型参数。梯度下降法在反向传播中的具体应用见下一小节。
3.4 反向传播原理(四个基础等式)
反向传播能够知道如何更改网络中的权重www 和偏差bbb 来改变代价函数值。最终这意味着它能够计算偏导数∂L(a[l],y)∂wjk[l]\frac{\partial L(a^{[l]},y)} {\partial w^{[l]}_{jk}}∂wjk[l]∂L(a[l],y) 和∂L(a[l],y)∂bj[l]\frac{\partial L(a^{[l]},y)}{\partial b^{[l]}_j} ∂bj[l]∂L(a[l],y)
为了计算这些偏导数,我们首先引入一个中间变量δj[l]\delta^{[l]}_jδj[l],我们把它叫做网络中第lthl^{th}lth层第jthj^{th}jth个神经元的误差。后向传播能够计算出误差δj[l]\delta^{[l]}_jδj[l],然后再将其对应回∂L(a[l],y)∂wjk[l]\frac{\partial L(a^{[l]},y)}{\partial w^{[l]}_{jk}}∂wjk[l]∂L(a[l],y)和∂L(a[l],y)∂bj[l]\frac{\partial L(a^{[l]},y)}{\partial b^{[l]}_j}∂bj[l]∂L(a[l],y) 。
那么,如何定义每一层的误差呢?如果为第lll 层第jjj 个神经元添加一个扰动Δzj[l]\Delta z^{[l]}_jΔzj[l],使得损失函数或者代价函数变小,那么这就是一个好的扰动。通过选择 Δzj[l]\Delta z^{[l]}_jΔzj[l]与∂L(a[l],y)∂zj[l]\frac{\partial L(a^{[l]}, y)}{\partial z^{[l]}_j}∂zj[l]∂L(a[l],y)符号相反(梯度下降法原理),就可以每次都添加一个好的扰动最终达到最优。
受此启发,我们定义网络层第lll 层中第jjj 个神经元的误差为δj[l]\delta^{[l]}_jδj[l]:
δj[l]=∂L(a[L],y)∂zj[l]\delta^{[l]}_j=\frac{\partial L(a^{[L], y})}{\partial z^{[l]}_j}δj[l]=∂zj[l]∂L(a[L],y)
于是,每一层的误差向量可以表示为:
δ[l]=[δ1[l]δ2[l]⋮δn[l]]\delta ^{[l]}=\left[\begin{array}{cc}\delta ^{[l]}_1\\\delta ^{[l]}_2\\ \vdots \\ \delta ^{[l]}_n\end{array} \right]δ[l]=⎣⎢⎢⎢⎢⎡δ1[l]δ2[l]⋮δn[l]⎦⎥⎥⎥⎥⎤
下面开始正式介绍四个基础等式【确切的说是四组等式】
**注意:**这里我们的输入为单个样本(所以我们在下面的公式中使用的是损失函数而不是代价函数)。多个样本输入的公式会在介绍完单个样本后再介绍。
- 等式1 :输出层误差
δj[L]=∂L∂aj[L]σ′(zj[L])\delta^{[L]}_j=\frac{\partial L}{\partial a^{[L]}_j}\sigma^{'}(z^{[L]}_j)δj[L]=∂aj[L]∂Lσ′(zj[L])
其中,LLL表示输出层层数。以下用∂L\partial L∂L 表示 ∂L(a[L],y)\partial L(a^{[L]}, y)∂L(a[L],y)
写成矩阵形式是:
δ[L]=∇aL⊙σ′(z[L])\delta^{[L]}=\nabla _aL\odot \sigma^{'}(z^{[L]})δ[L]=∇aL⊙σ′(z[L])
【注意是对应元素相乘,想想为什么?】
说明
根据本小节开始时的叙述,我们期望找到∂L/∂zj[l]\partial L \ /\partial z^{[l]}_j∂L /∂zj[l],然后朝着方向相反的方向更新网络参数,并定义误差为:
δj[L]=∂L∂zj[L]\delta^{[L]}_j=\frac{\partial L}{\partial z^{[L]}_j}δj[L]=∂zj[L]∂L
根据链式法则,
δj[L]=∑k∂L∂ak[L]∂ak[L]∂zj[L]\delta^{[L]}_j = \sum_k \frac{\partial L}{\partial a^{[L]}_k} \frac{\partial a^{[L]}_k}{\partial z^{[L]}_j}δj[L]=k∑∂ak[L]∂L∂zj[L]∂ak[L]
当k≠jk\neq jk̸=j时,∂ak[L]/∂zj[L]\partial a^{[L]}_k / \partial z^{[L]}_j∂ak[L]/∂zj[L]就为零。结果我们可以简化之前的等式为
δj[L]=∂L∂aj[L]∂aj[L]∂zj[L]\delta^{[L]}_j = \frac{\partial L}{\partial a^{[L]}_j} \frac{\partial a^{[L]}_j}{\partial z^{[L]}_j}δj[L]=∂aj[L]∂L∂zj[L]∂aj[L]
重新拿出定义:aj[L]=σ(zj[L])a^{[L]}_j = \sigma(z^{[L]}_j)aj[L]=σ(zj[L]),就可以得到:
δj[L]=∂L∂aj[L]σ′(zj[L])\delta^{[L]}_j = \frac{\partial L}{\partial a^{[L]}_j} \sigma'(z^{[L]}_j)δj[L]=∂aj[L]∂Lσ′(zj[L])
再"堆砌"成向量形式就得到了我们的矩阵表示式(这也是为什么使用矩阵形式表示需要 对应元素相乘 的原因)。
- 等式2: 隐含层误差
δj[l]=∑kwkj[l+1]δk[l+1]σ′(zj[l])\delta^{[l]}_j = \sum_k w^{[l+1]}_{kj} \delta^{[l+1]}_k \sigma'(z^{[l]}_j)δj[l]=k∑wkj[l+1]δk[l+1]σ′(zj[l])
写成矩阵形式:
δ[l]=[w[l+1]Tδ[l+1]]⊙σ′(z[l])\delta^{[l]}=[w^{[l+1]T}\delta^{[l+1]}]\odot \sigma ^{'}(z^{[l]})δ[l]=[w[l+1]Tδ[l+1]]⊙σ′(z[l])
说明:
zk[l+1]=∑jwkj[l+1]aj[l]+bk[l+1]=∑jwkj[l+1]σ(zj[l])+bk[l+1]z^{[l+1]}_k=\sum_jw^{[l+1]}_{kj}a^{[l]}_j+b^{[l+1]}_k=\sum_jw^{[l+1]}_{kj}\sigma(z^{[l]}_j)+b^{[l+1]}_kzk[l+1]=j∑wkj[l+1]aj[l]+bk[l+1]=j∑wkj[l+1]σ(zj[l])+bk[l+1]
进行偏导可以获得:
∂zk[l+1]∂zj[l]=wkj[l+1]σ′(zj[l])\frac{\partial z^{[l+1]}_k}{\partial z^{[l]}_j} = w^{[l+1]}_{kj} \sigma'(z^{[l]}_j)∂zj[l]∂zk[l+1]=wkj[l+1]σ′(zj[l])
代入得到:
δj[l]=∑kwkj[l+1]δk[l+1]σ′(zj[l])\delta^{[l]}_j = \sum_k w^{[l+1]}_{kj} \delta^{[l+1]}_k \sigma'(z^{[l]}_j)δj[l]=k∑wkj[l+1]δk[l+1]σ′(zj[l])
- 等式3:参数变化率
∂L∂bj[l]=δj[l]\frac{\partial L}{\partial b^{[l]}_j}=\delta^{[l]}_j∂bj[l]∂L=δj[l]
∂L∂wjk[l]=ak[l−1]δj[l]\frac{\partial L}{\partial w^{[l]}_{jk}}=a^{[l-1]}_k\delta^{[l]}_j∂wjk[l]∂L=ak[l−1]δj[l]
写成矩阵形式:
∂L∂b[l]=δ[l]\frac{\partial L}{\partial b^{[l]}}=\delta^{[l]}∂b[l]∂L=δ[l]∂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[l−1]T
说明:
根据链式法则推导。
由于
zj[l]=∑kwjk[l]ak[l]+bk[l]z^{[l]}_j=\sum_kw^{[l]}_{jk}a^{[l]}_k+b^{[l]}_kzj[l]=k∑wjk[l]ak[l]+bk[l]
对bj[l]b^{[l]}_jbj[l]求偏导得到:
∂L∂bj[l]=∂L∂zj[l]∂zj[l]bj[l]=δj[l]\frac{\partial L}{\partial b^{[l]}_j}=\frac{\partial L}{\partial z^{[l]}_j}\frac{\partial z^{[l]}_j}{b^{[l]}_j}=\delta^{[l]}_j∂bj[l]∂L=∂zj[l]∂Lbj[l]∂zj[l]=δj[l]
对wjk[l]w^{[l]}_{jk}wjk[l]求偏导得到:
∂L∂wjk[l]=∂L∂zj[l]∂zj[l]wjk[l]=ak[l−1]δj[l]\frac{\partial L}{\partial w^{[l]}_{jk}}=\frac{\partial L}{\partial z^{[l]}_j}\frac{\partial z^{[l]}_j}{w^{[l]}_{jk}}=a^{[l-1]}_k\delta^{[l]}_j∂wjk[l]∂L=∂zj[l]∂Lwjk[l]∂zj[l]=ak[l−1]δj[l]
最后再变成矩阵形式就好了。对矩阵形式来说,需要特别注意维度的匹配。强烈建议读者在自己编写程序之前,先列出这些等式,然后仔细检查维度是否匹配。
很容易看出∂L∂w[l]\frac{\partial L}{\partial w^{[l]}}∂w[l]∂L是一个dim(δ[l])dim(\delta^{[l]})dim(δ[l])行dim(a[l−1])dim(a^{[l-1]})dim(a[l−1])列的矩阵,和w[l]w^{[l]}w[l]的维度一致;∂L∂b[l]\frac{\partial L}{\partial b^{[l]}}∂b[l]∂L是一个维度为dim(δ[l])dim(\delta^{[l]})dim(δ[l])的列向量
- 等式4:参数更新规则
这应该是这四组公式里最简单的一组了,根据梯度下降法原理,朝着梯度的反方向更新参数:
bj[l]←bj[l]−α∂L∂bj[l]b^{[l]}_j\leftarrow b^{[l]}_j-\alpha \frac{\partial L}{\partial b^{[l]}_j}bj[l]←bj[l]−α∂bj[l]∂L
wjk[l]←wjk[l]−α∂L∂wjk[l]w^{[l]}_{jk}\leftarrow w^{[l]}_{jk}-\alpha\frac{\partial L}{\partial w^{[l]}_{jk}} wjk[l]←wjk[l]−α∂wjk[l]∂L
写成矩阵形式:
b[l]←b[l]−α∂L∂b[l]b^{[l]}\leftarrow b^{[l]}-\alpha\frac{\partial L}{\partial b^{[l]}}b[l]←b[l]−α∂b[l]∂L
w[l]←w[l]−α∂L∂w[l]w^{[l]}\leftarrow w^{[l]}-\alpha\frac{\partial L}{\partial w^{[l]}}w[l]←w[l]−α∂w[l]∂L
这里的α\alphaα指的是学习率。学习率指定了反向传播过程中梯度下降的步长。
3.5 反向传播总结
我们可以得到如下最终公式:
3.5.1 单样本输入公式表
说明 | 公式 | 备注 |
---|---|---|
输出层误差 | δ[L]=∇aL⊙σ′(z[L])\delta^{[L]}=\nabla _aL\odot \sigma^{'}(z^{[L]})δ[L]=∇aL⊙σ′(z[L]) | |
隐含层误差 | δ[l]=[w[l+1]Tδ[l+1]]⊙σ′(z[l])\delta^{[l]}=[w^{[l+1]T}\delta^{[l+1]}]\odot \sigma ^{'}(z^{[l]})δ[l]=[w[l+1]Tδ[l+1]]⊙σ′(z[l]) | |
参数变化率 | ∂L∂b[l]=δ[l]\frac{\partial L}{\partial b^{[l]}}=\delta^{[l]}∂b[l]∂L=δ[l]∂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[l−1]T | 注意维度匹配 |
参数更新 | b[l]←b[l]−α∂L∂b[l]b^{[l]}\leftarrow b^{[l]}-\alpha\frac{\partial L}{\partial b^{[l]}}b[l]←b[l]−α∂b[l]∂Lw[l]←w[l]−α∂L∂w[l]w^{[l]}\leftarrow w^{[l]}-\alpha\frac{\partial L}{\partial w^{[l]}}w[l]←w[l]−α∂w[l]∂L | α\alphaα是学习率 |
3.5.2 多样本输入公式表
多样本:需要使用代价函数,如果有m个样本,那么由于代价函数有一个1m\frac{1}{m}m1的常数项,因此所有的参数更新规则都需要有一个1m\frac{1}{m}m1的前缀。
多样本同时输入的时候需要格外注意维度匹配,一开始可能觉得有点混乱,但是不断加深理解就会豁然开朗。
说明 | 公式 | 备注 |
---|---|---|
输出层误差 | dZ[L]=∇AC⊙σ′(Z[L])dZ^{[L]}=\nabla _AC\odot \sigma^{'}(Z^{[L]})dZ[L]=∇AC⊙σ′(Z[L]) | 此时dZ[l]dZ^{[l]}dZ[l]不再是一个列向量,变成了一个mmm列的矩阵,每一列都对应一个样本的向量 |
隐含层误差 | dZ[l]=[w[l+1]TdZ[l+1]]⊙σ′(Z[l])dZ^{[l]}=[w^{[l+1]T}dZ^{[l+1]}]\odot \sigma ^{'}(Z^{[l]})dZ[l]=[w[l+1]TdZ[l+1]]⊙σ′(Z[l]) | 此时dZ[l]dZ^{[l]}dZ[l]的维度是n×mn\times mn×m,nnn表示第l层神经元的个数,m表示样本数 |
参数变化率 | db[l]=∂C∂b[l]=1mmeanOfEachRow(dZ[l])dw[l]=∂C∂w[l]=1mdZ[l]A[l−1]Tdb^{[l]}=\frac{\partial C}{\partial b^{[l]}}=\frac{1}{m}meanOfEachRow(dZ^{[l]})\\dw^{[l]}=\frac{\partial C}{\partial w^{[l]}}=\frac{1}{m}dZ^{[l]}A^{[l-1]T}db[l]=∂b[l]∂C=m1meanOfEachRow(dZ[l])dw[l]=∂w[l]∂C=m1dZ[l]A[l−1]T | 更新b[l]b^{[l]}b[l]的时候需要对每行求均值; 注意维度匹配; mmm是样本个数 |
参数更新 | b[l]←b[l]−α∂C∂b[l]b^{[l]}\leftarrow b^{[l]}-\alpha\frac{\partial C}{\partial b^{[l]}}b[l]←b[l]−α∂b[l]∂Cw[l]←w[l]−α∂C∂w[l]w^{[l]}\leftarrow w^{[l]}-\alpha\frac{\partial C}{\partial w^{[l]}}w[l]←w[l]−α∂w[l]∂C | α\alphaα是学习率 |
3.5.3 关于超参数
通过前面的介绍,相信读者可以发现BP神经网络模型有一些参数是需要设计者给出的,也有一些参数是模型自己求解的。
那么,哪些参数是需要模型设计者确定的呢?
比如,学习率α\alphaα,隐含层的层数,每个隐含层的神经元个数,激活函数的选取,损失函数(代价函数)的选取等等,这些参数被称之为超参数。
其它的参数,比如权重矩阵www和偏置系数bbb在确定了超参数之后是可以通过模型的计算来得到的,这些参数称之为普通参数,简称参数。
超参数的确定其实是很困难的。因为你很难知道什么样的超参数会让模型表现得更好。比如,学习率太小可能造成模型收敛速度过慢,学习率太大又可能造成模型不收敛;再比如,损失函数的设计,如果损失函数设计不好的话,可能会造成模型无法收敛;再比如,层数过多的时候,如何设计网络结构以避免梯度消失和梯度爆炸……
神经网络的程序比一般程序的调试难度大得多,因为它并不会显式报错,它只是无法得到你期望的结果,作为新手也很难确定到底哪里出了问题(对于自己设计的网络,这种现象尤甚,我目前也基本是新手,所以这些问题也在困扰着我)。当然,使用别人训练好的模型来微调看起来是一个捷径……
总之,神经网络至少在目前来看感觉还是黑箱的成分居多,希望通过大家的努力慢慢探索吧。
4. 是不是猫?
本小节主要使用上述公式来完成一个小例子,这个小小的神经网络可以告诉我们一张图片是不是猫。本例程参考了coursera的作业,有改动。
在实现代码之前,先把用到的公式列一个表格吧,这样对照着看大家更清晰一点(如果你没有2个显示器建议先把这些公式抄写到纸上,以便和代码对照):
编号 | 公式 | 备注 |
---|---|---|
1 | Z[l]=w[l]A[l−1]+b[l]Z^{[l]}=w^{[l]}A^{[l-1]}+b^{[l]}Z[l]=w[l]A[l−1]+b[l] | |
2 | A[l]=σ(Z[l])A^{[l]}=\sigma(Z^{[l]})A[l]=σ(Z[l]) | |
3 | dZ[L]=∇AC⊙σ′(Z[L])dZ^{[L]}=\nabla_AC\odot\sigma^{'}(Z^{[L]})dZ[L]=∇AC⊙σ′(Z[L]) | |
4 | dZ[l]=[w[l+1]TdZ[l+1]]⊙σ′(Z[l])dZ^{[l]}=[w^{[l+1]T}dZ^{[l+1]}]\odot \sigma ^{'}(Z^{[l]})dZ[l]=[w[l+1]TdZ[l+1]]⊙σ′(Z[l]) | |
5 | db[l]=∂C∂b[l]=1mmeanOfEachRow(dZ[l])db^{[l]}=\frac{\partial C}{\partial b^{[l]}}=\frac{1}{m}meanOfEachRow(dZ^{[l]})db[l]=∂b[l]∂C=m1meanOfEachRow(dZ[l]) | |
6 | dw[l]=∂C∂w[l]=1mdZ[l]A[l−1]Tdw^{[l]}=\frac{\partial C}{\partial w^{[l]}}=\frac{1}{m}dZ^{[l]}A^{[l-1]T}dw[l]=∂w[l]∂C=m1dZ[l]A[l−1]T | |
7 | b[l]←b[l]−α⋅db[l]b^{[l]}\leftarrow b^{[l]}-\alpha \cdot db^{[l]}b[l]←b[l]−α⋅db[l] | |
8 | w[l]←w[l]−α⋅dw[l]w^{[l]}\leftarrow w^{[l]}-\alpha\cdot dw^{[l]}w[l]←w[l]−α⋅dw[l] | |
9 | dA[l]=w[l]T⊙dZ[l]dA^{[l]}=w^{[l]T}\odot dZ^{[l]}dA[l]=w[l]T⊙dZ[l] | |
准备工作做的差不多了,让我们开始吧?等等,好像我们还没有定义代价函数是什么?OMG!好吧,看来我们得先把这个做好再继续了。
那先看结果吧,我们的代价函数是:
C=−1m∑i=1m(y(i)log(a[L](i))+(1−y(i))log(1−a[L](i)))C =-\frac{1}{m} \sum^{m}_{i=1}(y^{(i)}log(a^{[L](i)})+(1-y^{(i)})log(1-a^{[L](i)}))C=−m1i=1∑m(y(i)log(a[L](i))+(1−y(i))log(1−a[L](i)))
其中,mmm是样本数量;
下面简单介绍一下这个代价函数是怎么来的(作者非数学专业,不严谨的地方望海涵)。
.
代价函数的确定用到了统计学中的**“极大似然法”**,既然这样,那就不可避免地要介绍一下“极大似然法”了。极大似然法简单来说就是“在模型已定,参数未知的情况下,根据结果估计模型中参数的一种方法",换句话说,极大似然法提供了一种给定观察数据来评估模型参数的方法。
举个例子(本例参考了知乎相关回答),一个不透明的罐子里有黑白两种球(球仅仅颜色不同,大小重量等参数都一样)。有放回地随机拿出一个小球,记录颜色。重复10次之后发现7次是黑球,3次是白球。问你罐子里白球的比例?
相信很多人可以一口回答“30%”,那么,为什么呢?背后的原理是什么呢?
这里我们把每次取出一个球叫做一次抽样,把“抽样10次,7次黑球,3次白球”这个事件发生的概率记为P(事件结果∣Model)P(事件结果|Model)P(事件结果∣Model),我们的Model需要一个参数ppp表示白球的比例。那么P(事件结果∣Model)=p3(1−p)7P(事件结果|Model)=p^3(1-p)^7P(事件结果∣Model)=p3(1−p)7。
好了,现在我们已经有事件结果的概率公式了,接下来求解模型参数ppp,根据极大似然法的思想,既然这个事件发生了,那么为什么不让这个事件(抽样10次,7次黑球,3次白球)发生的概率最大呢?因为显然概率大的事件发生才是合理的。于是就变成了求解p3(1−p)7p^3(1-p)^7p3(1−p)7取最大值的ppp,即导数为0,经过求导:
d(p3(1−p)7)=3p2(1−p)7−7p3(1−p)6=p2(1−p)6(3−10p)=0d(p^3(1-p)^7)=3p^2(1-p)^7-7p^3(1-p)^6=p^2(1-p)^6(3-10p)=0d(p3(1−p)7)=3p2(1−p)7−7p3(1−p)6=p2(1−p)6(3−10p)=0
求解可得p=0.3p=0.3p=0.3
极大似然法有一个重要的假设:
假设所有样本独立同分布!!!
好了,现在来看看我们的神经网络模型。
最后一层我们用sigmoid函数求出一个激活输出a,如果a大于0.5,就表示这个图片是猫(y=1y=1y=1),否则就不是猫(y=0y=0y=0)。因此:
P(y=1∣x;θ)=aP(y=1|x;\theta)=aP(y=1∣x;θ)=a
P(y=0∣x;θ)=1−aP(y=0|x;\theta)=1-aP(y=0∣x;θ)=1−a
公式解释:
上述第一个公式表示,给定模型参数θ\thetaθ和输入xxx,是猫的概率是P(y=1∣x;θ)=aP(y=1|x;\theta)=aP(y=1∣x;θ)=a
把两个公式合并成一个公式,即
p(y∣x;θ)=ay(1−a)(1−y)p(y|x;\theta)=a^y(1-a)^{(1-y)}p(y∣x;θ)=ay(1−a)(1−y)
这里的θ\thetaθ指的就是我们神经网络的权值参数和偏置参数。
那么似然函数
L(θ)=p(Y∣X;θ)=∏i=1mp(y(i)∣x(i);θ)=∏i=1m(a[L](i))y(i)(1−a[L](i))(1−y(i))L(\theta)=p(Y|X;\theta)=\prod^m_{i=1}p(y^{(i)}|x^{(i)};\theta)=\prod^m_{i=1}(a^{[L](i)})^{y^{(i)}}(1-a^{[L](i)})^{(1-y^{(i)})}L(θ)=p(Y∣X;θ)=i=1∏mp(y(i)∣x(i);θ)=i=1∏m(a[L](i))y(i)(1−a[L](i))(1−y(i))
变成对数形式:
log(L(θ))=∑i=1m(y(i)log(a[L](i))+(1−y(i))log(1−a[L](i)))log(L(\theta))=\sum^m_{i=1}(y^{(i)}log(a^{[L](i)})+(1-y^{(i)})log(1-a^{[L](i)}))log(L(θ))=i=1∑m(y(i)log(a[L](i))+(1−y(i))log(1−a[L](i)))
所以我们的目标就是最大化这个对数似然函数,也就是最小化我们的代价函数:
C=−1m∑i=1m(y(i)log(a[L](i))+(1−y(i))log(1−a[L](i)))C =-\frac{1}{m} \sum^{m}_{i=1}(y^{(i)}log(a^{[L](i)})+(1-y^{(i)})log(1-a^{[L](i)}))C=−m1i=1∑m(y(i)log(a[L](i))+(1−y(i))log(1−a[L](i)))
其中,mmm是样本数量;
好了,终于可以开始写代码了,码字手都有点酸了,不得不说公式真的好难打。
由于代码比较简单就没有上传github。本文代码和数据文件可以在这里下载: https://pan.baidu.com/s/1q_PzaCSXOhRLOJVF5-vy2Q,密码: d7vx
其他下载源:
https://drive.google.com/file/d/0B6exrzrSxlh3TmhSV0ZNeHhYUmM/view?usp=sharing
4.1 辅助函数
辅助函数主要包括激活函数以及激活函数的反向传播过程函数:
其中,激活函数反向传播代码对应公式4和9.
def sigmoid(z):"""使用numpy实现sigmoid函数参数:Z numpy array输出:A 激活值(维数和Z完全相同)"""return 1/(1 + np.exp(-z))def relu(z):"""线性修正函数relu参数:z numpy array输出:A 激活值(维数和Z完全相同)"""return np.array(z>0)*zdef sigmoidBackward(dA, cacheA):"""sigmoid的反向传播参数:dA 同层激活值cacheA 同层线性输出输出:dZ 梯度"""s = sigmoid(cacheA)diff = s*(1 - s)dZ = dA * diffreturn dZdef reluBackward(dA, cacheA):"""relu的反向传播参数:dA 同层激活值cacheA 同层线性输出输出:dZ 梯度"""Z = cacheAdZ = np.array(dA, copy=True) dZ[Z <= 0] = 0return dZ
另外一个重要的辅助函数是数据读取函数和参数初始化函数:
def loadData(dataDir):"""导入数据参数:dataDir 数据集路径输出:训练集,测试集以及标签"""train_dataset = h5py.File(dataDir+'/train.h5', "r")train_set_x_orig = np.array(train_dataset["train_set_x"][:]) # your train set featurestrain_set_y_orig = np.array(train_dataset["train_set_y"][:]) # your train set labelstest_dataset = h5py.File(dataDir+'/test.h5', "r")test_set_x_orig = np.array(test_dataset["test_set_x"][:]) # your test set featurestest_set_y_orig = np.array(test_dataset["test_set_y"][:]) # your test set labelsclasses = np.array(test_dataset["list_classes"][:]) # the list of classestrain_set_y_orig = train_set_y_orig.reshape((1, train_set_y_orig.shape[0]))test_set_y_orig = test_set_y_orig.reshape((1, test_set_y_orig.shape[0]))return train_set_x_orig, train_set_y_orig, test_set_x_orig, test_set_y_orig, classesdef iniPara(laydims):"""随机初始化网络参数参数:laydims 一个python list输出:parameters 随机初始化的参数字典(”W1“,”b1“,”W2“,”b2“, ...)"""np.random.seed(1)parameters = {}for i in range(1, len(laydims)):parameters['W'+str(i)] = np.random.randn(laydims[i], laydims[i-1])/ np.sqrt(laydims[i-1])parameters['b'+str(i)] = np.zeros((laydims[i], 1))return parameters
4.2 前向传播过程
对应公式1和2.
def forwardLinear(W, b, A_prev):"""前向传播"""Z = np.dot(W, A_prev) + bcache = (W, A_prev, b)return Z, cachedef forwardLinearActivation(W, b, A_prev, activation):"""带激活函数的前向传播"""Z, cacheL = forwardLinear(W, b, A_prev)cacheA = Zif activation == 'sigmoid':A = sigmoid(Z)if activation == 'relu':A = relu(Z)cache = (cacheL, cacheA)return A, cachedef forwardModel(X, parameters):"""完整的前向传播过程"""layerdim = len(parameters)//2caches = []A_prev = Xfor i in range(1, layerdim):A_prev, cache = forwardLinearActivation(parameters['W'+str(i)], parameters['b'+str(i)], A_prev, 'relu')caches.append(cache)AL, cache = forwardLinearActivation(parameters['W'+str(layerdim)], parameters['b'+str(layerdim)], A_prev, 'sigmoid')caches.append(cache)return AL, caches
4.3 反向传播过程
线性部分反向传播对应公式5和6。
def linearBackward(dZ, cache):"""线性部分的反向传播参数:dZ 当前层误差cache (W, A_prev, b)元组输出:dA_prev 上一层激活的梯度dW 当前层W的梯度db 当前层b的梯度"""W, A_prev, b = cachem = A_prev.shape[1]dW = 1/m*np.dot(dZ, A_prev.T)db = 1/m*np.sum(dZ, axis = 1, keepdims=True)dA_prev = np.dot(W.T, dZ)return dA_prev, dW, db
非线性部分对应公式3、4、5和6 。
def linearActivationBackward(dA, cache, activation):"""非线性部分的反向传播参数:dA 当前层激活输出的梯度cache (W, A_prev, b)元组activation 激活函数类型输出:dA_prev 上一层激活的梯度dW 当前层W的梯度db 当前层b的梯度"""cacheL, cacheA = cacheif activation == 'relu':dZ = reluBackward(dA, cacheA)dA_prev, dW, db = linearBackward(dZ, cacheL)elif activation == 'sigmoid':dZ = sigmoidBackward(dA, cacheA)dA_prev, dW, db = linearBackward(dZ, cacheL)return dA_prev, dW, db
完整反向传播模型:
def backwardModel(AL, Y, caches):"""完整的反向传播过程参数:AL 输出层结果Y 标签值caches 【cacheL, cacheA】输出:diffs 梯度字典"""layerdim = len(caches)Y = Y.reshape(AL.shape)L = layerdimdiffs = {}dAL = - (np.divide(Y, AL) - np.divide(1 - Y, 1 - AL))currentCache = caches[L-1]dA_prev, dW, db = linearActivationBackward(dAL, currentCache, 'sigmoid')diffs['dA' + str(L)], diffs['dW'+str(L)], diffs['db'+str(L)] = dA_prev, dW, dbfor l in reversed(range(L-1)):currentCache = caches[l]dA_prev, dW, db = linearActivationBackward(dA_prev, currentCache, 'relu')diffs['dA' + str(l+1)], diffs['dW'+str(l+1)], diffs['db'+str(l+1)] = dA_prev, dW, dbreturn diffs
4.4 测试结果
打开你的jupyter notebook,运行我们的BP.ipynb文件,首先导入依赖库和数据集,然后使用一个循环来确定最佳的迭代次数大约为2000:
【图6】
最后用一个例子来看一下模型的效果——判断一张图片是不是猫:
【图7】
好了,测试到此结束。你也可以自己尝试其它的神经网络结构和测试其它图片。
5. 本文小结
本文主要叙述了经典的全连接神经网络结构以及前向传播和反向传播的过程。通过本文的学习,读者应该可以独立推导全连接神经网络的传播过程,对算法的细节烂熟于心。另外,由于本文里的公式大部分是我自己推导的,瑕疵之处,希望读者不吝赐教。
虽然这篇文章实现的例子并没有什么实际应用场景,但是自己推导一下这些数学公式并用代码实现对理解神经网络内部的原理很有帮助,继这篇博客之后,我还计划写一个如何自己推导并实现卷积神经网络的教程,如果有人感兴趣,请继续关注我!
本次内容就到这里,谢谢大家。
订正与答疑:
前向传播过程比较简单,我就不再赘述了。
这里主要针对反向传播过程中可能会出现的问题做一个总结:
1. 具体解释一下公式1里面的“堆砌”是什么意思?
δj[L]=∑k∂L∂ak[L]∂ak[L]∂zj[L]\delta^{[L]}_j = \sum_k \frac{\partial L}{\partial a^{[L]}_k} \frac{\partial a^{[L]}_k}{\partial z^{[L]}_j}δj[L]=k∑∂ak[L]∂L∂zj[L]∂ak[L]
有读者对这里不太理解,这其实是因为,我们的输出层不一定是只有一个神经元,可能有好多个神经元,因此损失函数是每个输出神经元“误差”之和,因此才会出现这种∑\sum∑的形式,然后每个输出神经元的误差函数与其它神经元没有关系,所以只有k=jk=jk=j的时候值不是0.
另外,这里说的“堆砌”指的就是:
δ[l]=[∂L∂a1[L]∂L∂a2[L]⋮]⊙[σ′(z1[L])σ′(z2[L])⋮]\delta^{[l]}=\left[ \begin{array}{cc} \frac{\partial L}{\partial a^{[L]}_1} \\ \frac{\partial L}{\partial a^{[L]}_2} \\\vdots \end{array}\right]\odot\left[\begin{array}{cc}\sigma^{'}(z^{[L]}_1) \\\sigma^{'}(z^{[L]}_2)\\\vdots\end{array}\right]δ[l]=⎣⎢⎢⎡∂a1[L]∂L∂a2[L]∂L⋮⎦⎥⎥⎤⊙⎣⎢⎢⎡σ′(z1[L])σ′(z2[L])⋮⎦⎥⎥⎤
2. 公式2写成矩阵形式为什么系数矩阵会有转置?自己没搞懂。
这里可能有一点绕,有的读者感觉我的推导不是很明白,所以有必要详细说明一下。
很多读者不明白,写成矩阵形式的时候
δ[l]=[w[l+1]Tδ[l+1]]⊙σ′(z[l])\delta^{[l]}=[w^{[l+1]T}\delta^{[l+1]}]\odot \sigma ^{'}(z^{[l]})δ[l]=[w[l+1]Tδ[l+1]]⊙σ′(z[l])
里面的“系数矩阵转置”是怎么来的。这里就主要说明一下:
相信大家都已经理解了下面这个前向传播公式:
zk[l+1]=∑jwkj[l+1]aj[l]+bk[l+1]=∑jwkj[l+1]σ(zj[l])+bk[l+1]z^{[l+1]}_k=\sum_jw^{[l+1]}_{kj}a^{[l]}_j+b^{[l+1]}_k=\sum_jw^{[l+1]}_{kj}\sigma(z^{[l]}_j)+b^{[l+1]}_kzk[l+1]=j∑wkj[l+1]aj[l]+bk[l+1]=j∑wkj[l+1]σ(zj[l])+bk[l+1]
求偏导这里在原文中有一点错误,应该是:
∂zk[l+1]∂zj[l]=∑kwkj[l+1]σ′(zj[l])\frac{\partial z^{[l+1]}_k}{\partial z^{[l]}_j} =\sum_k w^{[l+1]}_{kj} \sigma'(z^{[l]}_j)∂zj[l]∂zk[l+1]=k∑wkj[l+1]σ′(zj[l])
为了大家有一个直观的感受,来一个具体的例子:
第 1 层的系数矩阵比方是:
w[2=[w11[2]w12[2]w13[2]w21[2]w22[2]w23[2]]w^{[2}=\left[ \begin{array}{cc} w_{11}^{[2]} & w_{12}^{[2]} & w_{13}^{[2]} \\ w_{21}^{[2]}& w_{22}^{[2]} & w_{23}^{[2]}\end{array}\right]w[2=[w11[2]w21[2]w12[2]w22[2]w13[2]w23[2]]
b[2]=[b1[2]b2[2]b^{[2]}=\left[ \begin{array}{cc}b^{[2]}_1 \\ b^{[2}_2 \end{array}\right]b[2]=[b1[2]b2[2]
z[2]=[w11[2]w12[2]w13[2]w21[2]w22[2]w23[2]]⋅[a1[1]a2[1]a3[1]]+[b1[2]b2[2]]=[w11[2]a1[1]+w12[2]a2[1]+w13[2]a3[1]+b1[2]w21[2]a1[1]+w22[2]a2[1]+w23[2]a3[1]+b2[2]]z^{[2]}=\left[ \begin{array}{cc} w_{11}^{[2]} & w_{12}^{[2]} & w_{13}^{[2]} \\ w_{21}^{[2]}& w_{22}^{[2]} & w_{23}^{[2]}\end{array}\right]\cdot \left[ \begin{array}{cc} a^{[1]}_1 \\ a^{[1]}_2 \\ a^{[1]}_3 \end{array}\right] +\left[ \begin{array}{cc}b^{[2]}_1 \\ b^{[2]}_2 \end{array}\right]=\left[ \begin{array}{cc} w_{11}^{[2]}a^{[1]}_1+w_{12}^{[2]}a^{[1]}_2+w_{13}^{[2]}a^{[1]}_3+b^{[2]}_1 \\ w^{[2]}_{21}a^{[1]}_1+w_{22}^{[2]}a^{[1]}_2+w_{23}^{[2]}a^{[1]}_3+b^{[2]}_2\end{array}\right]z[2]=[w11[2]w21[2]w12[2]w22[2]w13[2]w23[2]]⋅⎣⎢⎡a1[1]a2[1]a3[1]⎦⎥⎤+[b1[2]b2[2]]=[w11[2]a1[1]+w12[2]a2[1]+w13[2]a3[1]+b1[2]w21[2]a1[1]+w22[2]a2[1]+w23[2]a3[1]+b2[2]]
那么,
∂z1[2]∂a1[1]=∂(w11[2]a1[1]+w12[2]a2[1]+w13[2]a3[1]+b1[2])∂a1[1]=w11[2]\frac{\partial z^{[2]}_1}{\partial a^{[1]}_1}=\frac{\partial(w_{11}^{[2]}a^{[1]}_1+w_{12}^{[2]}a^{[1]}_2+w_{13}^{[2]}a^{[1]}_3+b^{[2]}_1)}{\partial a^{[1]}_1}=w^{[2]}_{11}∂a1[1]∂z1[2]=∂a1[1]∂(w11[2]a1[1]+w12[2]a2[1]+w13[2]a3[1]+b1[2])=w11[2]
那么,根据之前介绍的求解梯度向量的定义:
∂z1[2]∂a[1]=[∂z1[2]∂a1[1]∂z1[2]∂a2[1]∂z1[2]∂a3[1]]=[w11[2]w12[2]w13[2]]\frac{\partial z^{[2]}_1}{\partial a^{[1]}}=\left[ \begin{array}{cc}\frac{\partial z^{[2]}_1}{\partial a^{[1]}_1}\\\frac{\partial z^{[2]}_1}{\partial a^{[1]}_2}\\\frac{\partial z^{[2]}_1}{\partial a^{[1]}_3}\end{array}\right]=\left[\begin{array}{cc}w^{[2]}_{11}\\w^{[2]}_{12}\\w^{[2]}_{13}\end{array}\right]∂a[1]∂z1[2]=⎣⎢⎢⎢⎢⎡∂a1[1]∂z1[2]∂a2[1]∂z1[2]∂a3[1]∂z1[2]⎦⎥⎥⎥⎥⎤=⎣⎢⎡w11[2]w12[2]w13[2]⎦⎥⎤
∂z2[2]∂a[1]=[∂z2[2]∂a1[1]∂z2[2]∂a2[1]∂z2[2]∂a3[1]]=[w21[2]w22[2]w23[2]]\frac{\partial z^{[2]}_2}{\partial a^{[1]}}=\left[ \begin{array}{cc}\frac{\partial z^{[2]}_2}{\partial a^{[1]}_1}\\\frac{\partial z^{[2]}_2}{\partial a^{[1]}_2}\\\frac{\partial z^{[2]}_2}{\partial a^{[1]}_3}\end{array}\right]=\left[\begin{array}{cc}w^{[2]}_{21}\\w^{[2]}_{22}\\w^{[2]}_{23}\end{array}\right]∂a[1]∂z2[2]=⎣⎢⎢⎢⎢⎡∂a1[1]∂z2[2]∂a2[1]∂z2[2]∂a3[1]∂z2[2]⎦⎥⎥⎥⎥⎤=⎣⎢⎡w21[2]w22[2]w23[2]⎦⎥⎤
这就解释了,为什么会出现转置了。
然后排布成矩阵形式:
[w11[2]w21[2]w12[2]w22[2]w13[2]w23[2]]\left[\begin{array}{cc}w^{[2]}_{11}&w^{[2]}_{21}\\w^{[2]}_{12}&w^{[2]}_{22}\\w^{[2]}_{13}&w^{[2]}_{23}\end{array}\right]⎣⎢⎡w11[2]w12[2]w13[2]w21[2]w22[2]w23[2]⎦⎥⎤
或者,根据推到得到的公式:δj[l]=∑kwkj[l+1]δk[l+1]σ′(zj[l])\delta^{[l]}_j = \sum_k w^{[l+1]}_{kj} \delta^{[l+1]}_k \sigma'(z^{[l]}_j)δj[l]=∑kwkj[l+1]δk[l+1]σ′(zj[l]) 写成矩阵形式:
[δ1[1]δ2[1]δ3[1]]=[w11[2]w21[2]w12[2]w22[2]w13[2]w23[2]][δ1[2]δ2[2]]⊙[σ′(z1[1])σ′(z2[1])σ′(z3[1])]\left[\begin{array}{cc}\delta^{[1]}_1\\\delta^{[1]}_2\\\delta^{[1]}_3\end{array}\right]=\left[\begin{array}{cc}w^{[2]}_{11}&w^{[2]}_{21}\\w^{[2]}_{12}&w^{[2]}_{22}\\w^{[2]}_{13}&w^{[2]}_{23}\end{array}\right]\left[\begin{array}{cc}\delta^{[2]}_1\\\delta^{[2]}_2\end{array}\right]\odot\left[\begin{array}{cc}\sigma^{'}(z^{[1]}_1)\\\sigma^{'}(z^{[1]}_2)\\\sigma^{'}(z^{[1]}_3)\end{array}\right]⎣⎢⎡δ1[1]δ2[1]δ3[1]⎦⎥⎤=⎣⎢⎡w11[2]w12[2]w13[2]w21[2]w22[2]w23[2]⎦⎥⎤[δ1[2]δ2[2]]⊙⎣⎢⎡σ′(z1[1])σ′(z2[1])σ′(z3[1])⎦⎥⎤
也可以解释为什那么会变成转置。
写成矩阵形式,注意检查一下维度匹配的问题。
3. 公式3能具体讲一下矩阵形式是怎么来的吗?
这里有一点小错误,说明部分的第一个公式应该是:
zj[l]=∑kwjk[l]ak[l−1]+bk[l]z^{[l]}_j=\sum_kw^{[l]}_{jk}a^{[l-1]}_k+b^{[l]}_kzj[l]=k∑wjk[l]ak[l−1]+bk[l]
对bbb求偏导的过程比较简单,这里就不再赘述。
主要详细解释一下对 www 的求导过程:
对系数矩阵单个系数元素的推导原文已经说得比较明白了∂L∂wjk[l]=∂L∂zj[l]∂zj[l]wjk[l]=ak[l−1]δj[l]\frac{\partial L}{\partial w^{[l]}_{jk}}=\frac{\partial L}{\partial z^{[l]}_j}\frac{\partial z^{[l]}_j}{w^{[l]}_{jk}}=a^{[l-1]}_k\delta^{[l]}_j∂wjk[l]∂L=∂zj[l]∂Lwjk[l]∂zj[l]=ak[l−1]δj[l],有些读者可能还是不清楚如何把单个元素的公式对应为矩阵形式的公式:
单个元素公式∂L∂wjk[l]=∂L∂zj[l]∂zj[l]wjk[l]=ak[l−1]δj[l]\frac{\partial L}{\partial w^{[l]}_{jk}}=\frac{\partial L}{\partial z^{[l]}_j}\frac{\partial z^{[l]}_j}{w^{[l]}_{jk}}=a^{[l-1]}_k\delta^{[l]}_j∂wjk[l]∂L=∂zj[l]∂Lwjk[l]∂zj[l]=ak[l−1]δj[l]说明系数矩阵w[l]w^{[l]}w[l]的第jthj^{th}jth行kthk^{th}kth列的值为ak[l−1]δj[l]a^{[l-1]}_k\delta^{[l]}_jak[l−1]δj[l],所以δj[l]\delta^{[l]}_jδj[l]对应行,ak[l−1]a^{[l-1]}_kak[l−1]对应列,就得到了我们的矩阵形式。这也解释了为什么会出现转置。
4. 为什么会损失函数不用最小二乘法?
有的读者问,为什么使用极大似然估计,而不用最小二乘法?
其实在线性回归模型中,损失函数也是使用极大似然法来估计的,http://www.jianshu.com/p/0d25be8901c9,只不过线性回归模型中,我们假设残差是高斯分布,因此最终使用极大似然法和最小二乘法的结果是一样的。
如果你直接使用误差平方和最小的话,也不是不可以,但是效果可能会比较差。因为他是非凸的,可能会收敛到局部最优解。
而使用对数似然函数作为损失函数,是凸函数。
5. 能不能顺便介绍一下Python环境的管理?
一般我用anaconda管理Python环境,另外IDE推荐spyder,因为你可以像使用MATLAB的workspace一样直接观察中间变量,对初学者检查自己的程序的正确性很有帮助。
6. 为什么w的初始化使用随机初始化,而b参数的初始化全部初始化为0?
首先,b参数也可以用随机初始化。
为什么不把w全部初始化为0呢?因为这样的话,每次学习所有的隐含神经元学到的东西都一样,最终会导致,和层一个神经元没有区别,所以我们需要随机初始化开打破这种局面,让每个神经元都更好地“学习“。
一文搞定BP神经网络——从原理到应用(原理篇)相关推荐
- 一文搞定bp神经网络,bp神经网络的实现
1.自学bp神经网络要有什么基础?? 简介:BP(Back Propagation)网络是1986年由Rumelhart和McCelland为首的科学家小组提出,是一种按误差逆传播算法训练的多层前馈网 ...
- BP神经网络的基本思想,一文搞定bp神经网络
自学bp神经网络要有什么基础?? . 简介:BP(BackPropagation)网络是1986年由Rumelhart和McCelland为首的科学家小组提出,是一种按误差逆传播算法训练的多层前馈网络 ...
- bp神经网络的主要功能,一文搞定bp神经网络
张力传感器的工作原理 张力传感器(tensionsensor):张力传感器是张力控制过程中,用于测量卷材张力值大小的仪器. 张力传感器适用于各种光纤.纱线.化纤等的张力测量:广泛应用于电子.化工.纺织 ...
- bp神经网络图像特征提取,一文搞定bp神经网络
本人新手,在做BP神经网络的时候遇到了一个问题 5 不知你是不是用matlab的神经网络工具箱,因为一般神经网络都是成批处理的,每一次调整都会综合所有样本的误差进行调整,而不是一类一类图片的去调整,所 ...
- 一文搞定Docker(内含docker-compose及docker核心原理)
01-Docker概述 Docker简介 Docker是基于Go语言实现的云开源项目. Docker的主要目标是: Build, Ship and Run Any App, Anywhere ,也就是 ...
- 【嵌入式开发-AD19】六文搞定Altium Designer-第一章:AD介绍及原理图库的创建
[嵌入式开发-AD19]六文搞定Altium Designer-第一章:AD介绍及原理图库的创建 在文章的开头我想首先简单介绍一下国产全免费EDA软件,嘉立创EDA.嘉立创EDA拥有网页版和安装版两种 ...
- 【语义分割】综述——一文搞定语义分割
本文记录了博主阅读的关于语义分割(Semantic Segmentation)的综述类文章的笔记,更新于2019.02.19. [语义分割]综述--一文搞定语义分割 参考文献网址 An overvie ...
- 一文搞定c++多线程同步机制
c++多线程同步机制 前序文章:一文搞定c++多线程 同步与互斥 现代操作系统都是多任务操作系统,通常同一时刻有大量可执行实体,则运行着的大量任务可能需要访问或使用同一资源,或者说这些任务之间具有依赖 ...
- php带参数单元测试_一文搞定单元测试核心概念
基础概念 单元测试(unittesting),是指对软件中的最小可测试单元进行检查和验证,这里的最小可测试单元通常是指函数或者类.单元测试是即所谓的白盒测试,一般由开发人员负责测试,因为开发人员知道被 ...
- 【Python基础】一文搞定pandas的数据合并
作者:来源于读者投稿 出品:Python数据之道 一文搞定pandas的数据合并 在实际处理数据业务需求中,我们经常会遇到这样的需求:将多个表连接起来再进行数据的处理和分析,类似SQL中的连接查询功能 ...
最新文章
- QT的QClipboard类的使用
- Day9-HTML body属性
- 简书粉丝列表老bug
- 谈谈MySQL的WriteSet并行复制
- maven 教程入门 maven 配置管理 编译java程序
- 3年后,基于mysql控制vsftp的用户认证机制
- Photoshop小技巧集锦八十条
- 热烈庆祝阳光网驿-行业软件交流平台与北京汉邦极通科技有限公司成功合作
- postman一直sending 常见问题
- 第三十二章 三更雪压飞狐城(二之全)
- 转: 雅虎35条优化黄金守则
- 10分钟用Python制作恋爱日志
- go库函数之-time-使用示例
- 求Fibonacci(斐波那契)数列的的前n项
- 大话设计模式——策略模式
- Unity接入百度语音识别SDK windows平台
- Linux - tail命令详解
- 计算机硬盘容量越大运行速度越快,固态硬盘容量越大运行速度越快吗 固态硬盘运行速度分析...
- 解决VM 与 Device/Credential Guard 不兼容问题
- c语言微信自动跳一跳,C/C++知识点之微信跳一跳辅助c++实现 轻松上万
热门文章
- bzoj3786: 星系探索 //ETT
- 待支付模板html,待支付.html
- python格式化字符串固定宽度_python – 格式化固定宽度的字符串(unicode和utf8)
- 企鹅F4手机外观设计有突破 配MTK6592八核处理器
- chrome去广告插件 去掉百度热搜
- 细粒度的、安全高效的基于区块链数据溯源系统 Fine Grained, Secure and Efficient Data Provenance on Blockchain Systems(一)
- FutureWarning: Passing (type, 1) or '1type' as a synonym of type is deprecate;的解决办法
- 【学习体会】Lighttools8.4.0:简单光学系统实例
- STEP2——《数据分析:企业的贤内助》重点摘要笔记(一)——方案部分
- linux 下进入root