概述
上一章主要描述了线性回归与逻辑回归的概念与区别,在模型优化方面带过了,本篇着重记录下深度学习中常用来作参数优化的反向传播算法,并结合一个简单的例子阐明。
首先需要明确的是,任何一个神经网络都可以看作是一个关于其输入的函数,这个函数接受一系列值作为其输入,并输出一个或者多个结果,如下:
(
y
0
,
y
1
.
.
.
y
m
)
=
f
(
x
1
,
x
2
.
.
.
x
n
)
(y_0,y_1...y_m)=f(x_1,x_2...x_n)
(y0,y1...ym)=f(x1,x2...xn)
简化为向量形式:
Y
=
f
(
X
)
Y=f(X)
Y=f(X)
其中m,n均可以为1或者任意值,此外,此函数中还包含一系列参数用于调整该模型的性能,因此该函数可以写成如下形式:
(
y
0
,
y
1
.
.
.
y
m
)
=
f
w
1
,
w
2
.
.
.
w
k
(
x
1
,
x
2
.
.
.
x
n
)
(y_0,y_1...y_m)=f_{w_1,w_2...w_k}(x_1,x_2...x_n)
(y0,y1...ym)=fw1,w2...wk(x1,x2...xn)
简化形式:
Y
=
f
(
X
,
W
)
Y=f(X,W)
Y=f(X,W)
例如常用的线性模型:
Y
=
W
X
+
B
Y=WX+B
Y=WX+B
因此,可以认为任意网络都仅由三个要素组成:输入、输出、参数。一般而言,输入是采集到的数据例如图像、语音等等,转化为数据作为输入,经过网络中与网络参数一系列的计算得到输出,显然一个未经训练的网络输出只是一系列没有规律的值,而网络训练的过程,实际就是采集一系列已知的输入和输出真值,将输入喂入网络,生成一个输出,并将网络的输出与采集的输出真值对比,以其差值在网络中进行反向传播来优化网络参数的过程。
实现
- 损失函数
损失函数的作用就是衡量网络输出与输出真值的差距,可以认为,当该差距越小时,网络输出与输出真值越接近,也越能够反应该组数据的规律,不同的网络会因为各种原因选用不同的损失函数,这里不做深入探究,损失函数可以简化如下:
l o s s = C ( f w 1 , w 2 . . . w k ( x 1 , x 2 . . . x n ) , y t r u t h ) loss=C(f_{w_1,w_2...w_k}(x_1,x_2...x_n),y_{truth}) loss=C(fw1,w2...wk(x1,x2...xn),ytruth)
简化:
l o s s = C ( f ( X , W ) , Y t r u t h ) loss=C(f(X,W),Y_{truth}) loss=C(f(X,W),Ytruth) - 梯度下降
因此可以认为网络训练的过程就是调整参数使loss不断趋于极小值的过程,有许多算法可以实现,这里主要介绍下常用的梯度下降法,其细节也有很多文章讲解,这里也不多作介绍,对于以上loss函数,在训练过程中,由于输入 x 1 , x 2 . . . x n x_1,x_2...x_n x1,x2...xn与 y t r u t h y_{truth} ytruth为提前采集到的数据,可以认为是已知值,将参数 w 1 , w 2 . . . w k w_1,w_2...w_k w1,w2...wk视为自变量,loss为因变量,其梯度如下:
( ∂ C ∂ w 1 , ∂ C ∂ w 2 , . . . ∂ C ∂ w k ) (\frac{\partial C}{\partial w_1},\frac{\partial C}{\partial w_2},...\frac{\partial C}{\partial w_k}) (∂w1∂C,∂w2∂C,...∂wk∂C)
加入学习率如下:
( Δ w 1 , Δ w 2 . . . Δ w k , ) = η ( ∂ C ∂ w 1 , ∂ C ∂ w 2 , . . . ∂ C ∂ w k ) (\Delta w_1,\Delta w_2...\Delta w_k,)=\eta(\frac{\partial C}{\partial w_1},\frac{\partial C}{\partial w_2},...\frac{\partial C}{\partial w_k}) (Δw1,Δw2...Δwk,)=η(∂w1∂C,∂w2∂C,...∂wk∂C)
因此,参数优化如下:
w 1 ′ = w 1 − Δ w 1 w 2 ′ = w 2 − Δ w 2 . . . w k ′ = w 1 − Δ w k w'_1=w_1-\Delta w_1\\ w'_2=w_2-\Delta w_2\\ ...\\ w'_k=w_1-\Delta w_k\\ w1′=w1−Δw1w2′=w2−Δw2...wk′=w1−Δwk
简单示例与代码
以一个简单逻辑回归为例,其网络结构如下:
表达式如下:
Z
1
=
W
1
X
1
+
B
1
Z
2
=
W
2
Z
1
+
B
2
Z^1=W_1X_1+B_1\\ Z^2=W_2Z^1+B_2
Z1=W1X1+B1Z2=W2Z1+B2
其激活函数为sigmoid:
σ
(
z
)
=
1
1
+
e
−
z
\sigma(z)=\frac{1}{1+e^{-z}}
σ(z)=1+e−z1
a
=
σ
(
z
1
2
)
a=\sigma(z_1^2)
a=σ(z12)
使用交叉熵作为损失函数:
C
=
y
l
a
b
e
l
l
o
g
(
a
)
+
(
1
−
y
l
a
b
e
l
)
l
o
g
(
1
−
a
)
C=y_{label}log(a)+(1-y_{label})log(1-a)
C=ylabellog(a)+(1−ylabel)log(1−a)
于是有:
∂
C
∂
a
=
y
l
a
b
e
l
a
−
1
−
y
l
a
b
e
l
1
−
a
∂
a
∂
z
1
2
=
a
(
1
−
a
)
∂
z
j
l
∂
w
i
j
(
l
−
1
)
=
z
i
(
l
−
1
)
∂
z
j
l
∂
b
j
(
l
−
1
)
=
1
\frac{\partial C}{\partial a}=\frac{y_{label}}{a}-\frac{1-y_{label}}{1-a}\\ \ \\ \frac{\partial a}{\partial z_1^2}=a(1-a)\\ \ \\ \frac{\partial z_j^l}{\partial w^{(l-1)}_{ij}}=z^{(l-1)}_i\\ \ \\ \frac{\partial z_j^l}{\partial b^{(l-1)}_j}=1\\
∂a∂C=aylabel−1−a1−ylabel ∂z12∂a=a(1−a) ∂wij(l−1)∂zjl=zi(l−1) ∂bj(l−1)∂zjl=1
以第1层参数更新为例:
∂
C
∂
w
11
1
=
∂
C
∂
a
∗
∂
a
∂
z
1
2
∗
∂
z
1
2
∂
w
11
1
=
∂
C
∂
z
1
2
∗
z
1
1
∂
C
∂
b
1
1
=
∂
C
∂
a
∗
∂
a
∂
z
1
2
∗
∂
z
1
2
∂
b
1
1
=
∂
C
∂
z
1
2
∗
1
\frac{\partial C}{\partial w_{11}^1}=\frac{\partial C}{\partial a}*\frac{\partial a}{\partial z_1^2}*\frac{\partial z_1^2}{\partial w^1_{11}}=\frac{\partial C}{\partial z_1^2}*z^1_1\\ \ \\ \frac{\partial C}{\partial b^{1}_1}=\frac{\partial C}{\partial a}*\frac{\partial a}{\partial z_1^2}*\frac{\partial z_1^2}{\partial b^{1}_1}=\frac{\partial C}{\partial z_1^2}*1
∂w111∂C=∂a∂C∗∂z12∂a∗∂w111∂z12=∂z12∂C∗z11 ∂b11∂C=∂a∂C∗∂z12∂a∗∂b11∂z12=∂z12∂C∗1
写成矩阵形式,令
δ
l
\delta^l
δl为C对l层z偏导的向量,即
δ
l
=
∂
C
∂
z
l
\delta^l=\frac{\partial C}{\partial z^l}
δl=∂zl∂C,于是可以推导得到以下公式:
δ
l
=
W
T
δ
l
+
1
∂
C
∂
w
l
=
δ
l
+
1
∗
Z
l
T
∂
C
∂
w
l
=
δ
l
+
1
\delta^l=W^T\delta^{l+1}\\ \ \\ \frac{\partial C}{\partial w^l}=\delta^{l+1}*Z^{lT}\\ \ \\ \frac{\partial C}{\partial w^l}=\delta^{l+1}
δl=WTδl+1 ∂wl∂C=δl+1∗ZlT ∂wl∂C=δl+1
由以上两个公式可以得知,在反向传播过程中,可以将
δ
l
+
1
\delta^{l+1}
δl+1视为第
l
l
l层的输入,
δ
l
\delta^{l}
δl为输出并作为
l
−
1
l-1
l−1层的输入继续传播,
δ
l
+
1
\delta^{l+1}
δl+1结合前向传播中得到的
z
l
z^l
zl的转置可以得到损失函数对于该层权重和偏移参数的偏导数,以此来更新该层的参数。
代码如下:
import numpy as np
from abc import ABC, abstractmethod
sigmoid = np.vectorize(lambda z: 1 / (1 + np.exp(-z)))
class Layer(ABC):
@abstractmethod
def front_pro(self, inputs):
pass
@abstractmethod
def back_pro(self, output_der):
pass
@abstractmethod
def update(self, length, eta):
pass
class DenseLayer(Layer):
def __init__(self, *shape):
self._weights = 0.1 * np.random.randn(*shape)
self._bias = np.random.randn(shape[0], 1)
self._temp_weight_delta = np.zeros_like(self._weights)
self._temp_bias_delta = np.zeros_like(self._bias)
self._input = np.array((shape[-1], 1))
self._output = np.array((shape[0], 1))
def front_pro(self, inputs):
self._input = inputs
# print(self._weights.shape)
# print(np.array(inputs).shape)
self._output = np.dot(self._weights, inputs) + self._bias
# self._output = np.swapaxes(self._output, 1, 0)
# print(self._output.shape)
return self._output
def back_pro(self, output_der):
weight_delta = np.dot(output_der, self._input.transpose())
self._temp_weight_delta += weight_delta
bias_delta = output_der
self._temp_bias_delta += bias_delta
return np.dot(self._weights.transpose(), output_der)
def update(self, length, eta):
self._weights += self._temp_weight_delta / length * eta
self._bias += self._temp_bias_delta / length * eta
self._temp_weight_delta = np.zeros_like(self._weights)
self._temp_bias_delta = np.zeros_like(self._bias)
class SigmoidLayer(Layer):
def __init__(self, last_layer=False):
self._output = np.array([])
self._last_layer = last_layer
def front_pro(self, inputs):
self._output = sigmoid(inputs)
return self._output
def back_pro(self, output_der):
if self._last_layer:
return output_der
return output_der * self._output * (1 - self._output)
def update(self, length, eta):
pass
class Network(object):
# loss func: y*log(a)+(1-y)*log(1-a)
def __init__(self):
self._z = np.ndarray((1,))
self._output = np.ndarray((1,))
self._weights = np.ndarray((1,))
self._bias = np.ndarray((1,))
self._layers = []
def add_layer(self, layer):
self._layers.append(layer)
def front_pro(self, inputs):
output = []
for input in inputs:
for layer in self._layers:
input = layer.front_pro(input)
output.append(input)
return output
def back_pro(self, output_der):
for layer in reversed(self._layers):
output_der = layer.back_pro(output_der)
def cross_entropy_der(self, output, label):
return label / output - (1 - label) / (1 - output)
def train(self, data_input, data_label, eta):
for x, y in zip(data_input, data_label):
x = np.expand_dims(x, 0)
output = self.front_pro(x)
output = output[0]
# output_der = self.cross_entropy_der(output, y)
output_der = y - output
self.back_pro(output_der)
for layer in self._layers:
layer.update(len(data_input), eta)
def main():
network = Network()
network.add_layer(DenseLayer(2, 2))
network.add_layer(DenseLayer(1, 2))
network.add_layer(SigmoidLayer(last_layer=True))
input_generator = lambda x: 5 * x + 20
inputs = []
for i in range(10):
if i < 5:
inputs.append(np.array([[i], [input_generator(i) + 20]]))
else:
inputs.append(np.array([[i], [input_generator(i) - 20]]))
y = np.array([1] * 5 + [0] * 5).reshape((10, 1))
print(np.array(network.front_pro(inputs)).reshape(10,))
for j in range(60):
for i in range(5):
network.train(inputs[i:i + 2], y[i:i + 2], 0.05)
print(np.array(network.front_pro(inputs)).reshape(10,))
if __name__ == '__main__':
main()
以上仅使用了10组简单的数据来训练,如下:
[[ 0 40] [ 1 45] [ 2 50] [ 3 55] [ 4 60] [ 5 25] [ 6 30] [ 7 35] [ 8 40] [ 9 45]]
对应label如下:
[1 1 1 1 1 0 0 0 0 0]
训练前网络输出结果:
[0.70424612 0.7159282 0.7273275 0.73843649 0.74924892 0.66834236
0.68079983 0.6930043 0.70494438 0.71660998]
训练后:
[0.99848587 0.99706174 0.99430577 0.98899335 0.97883024 0.03070642
0.01604001 0.00831864 0.00429797 0.00221627]