前面已经讲解了梯度下降的推导,接下来我们使用代码实现简单的梯度下降,其中包括
- 梯度下降
- 前向反馈
- 反向传播
在学习深度学习的时候,不可避免的要知道神经网络的工作原理,虽然我们可以直接使用各种开源库的API,但是我觉得知道神经网络的工作原理可以更好的帮助我们来构建我们的网络,调节参数,更好的理解我们的网络在做什么,废话不多说,下面开始我们的代码,再说一句废话,我也是初学者,是在学习了优达的课程后自己的总结,哪里不对的还望指教
误差函数
定一个网络后,怎么来衡量网络的好坏能,这个衡量标准就是误差函数,并且梯度下降也需要对误差函数求导来计算下降梯度,接下来我们使用两个误差函数来分别写代码,这里我不会做推导,推导过程可以参看梯度下降推导
平均平方误差
现在我们选择误差公式为平方误差,激活函数为 sigmoid 函数
误差函数: E r r o r = 1 2 m ∑ i = 1 m ( y − y ^ ) 2 Error = \frac{1}{2m} \sum_{i=1}^{m}(y -\hat{y})^2 Error=2m1∑i=1m(y−y^)2
激活函数: σ ( z ) = 1 1 + e − z \sigma (z) = \frac{1}{1 + e^{-z}} σ(z)=1+e−z1
预测函数: y ( i ) ^ = σ ( W T x ( i ) + b ) \hat{y^{(i)}} = \sigma (W^Tx{(i)} + b) y(i)^=σ(WTx(i)+b)
激活函数导数: s i g m a ′ ( z ) = σ ( z ) ∗ ( 1 − σ ( z ) ) sigma'(z) = \sigma (z)*(1-\sigma (z)) sigma′(z)=σ(z)∗(1−σ(z))
误差函数导数: E r r o r ′ = ( y − y ^ ) Error' = (y -\hat{y}) Error′=(y−y^)
线性方程公式:z = W*X
误差函数对权重求偏导数: ϑ E ( w i , y ) ϑ w i = ( y − y ^ ) ( σ ( z ) ∗ ( 1 − σ ( z ) ) ) x i \frac{\vartheta E(w_i, y)}{\vartheta w_i} = (y - \hat{y})( \sigma (z)*(1-\sigma (z)))x_i ϑwiϑE(wi,y)=(y−y^)(σ(z)∗(1−σ(z)))xi
import numpy as np
# Defining the sigmoid function for activations
def sigmoid(x):
return 1 / ( 1 + np.exp(-x) )
# Derivative of the sigmoid function 导数
def sigmoid_prime(x):
return sigmoid(x) * (1 - sigmoid(x))
# 学习速率
learnrate = 0.5
# Input data
x = np.array([1, 2, 3, 4])
y = np.array(0.5)
print('input data : ', x)
print('output data: ', y)
# Initial weights
w = np.array([0.5, -0.5, 0.3, 0.1])
print('initial weights : ', w)
# 计算输入数据和权重的值
h = np.dot(x, w)
print('线性方程的输出值 : ', h)
# 计算网络输出值
nn_output = sigmoid(h)
print('网络输出 :', nn_output)
# 计算网络误差
error = y-nn_output
print('网络误差:', error)
# 计算误差项
error_term = error*sigmoid_prime(h)
print('error term : ', error_term)
# 更新权重
del_w = learnrate * error_term * x
print('Neural Network output: ')
print('nn_output : ', nn_output)
print('Amount of Error: ', error)
print('change in weights: ', del_w)
交叉熵误差
现在我们选择误差公式为平方误差,激活函数为 sigmoid 函数
损失函数: E r r o r = − ( y ln y ^ + ( 1 − y ) ln ( 1 − y ^ ) ) Error = -(y\ln\hat{y} + (1-y)\ln(1-\hat{y})) Error=−(ylny^+(1−y)ln(1−y^))
激活函数: σ ( z ) = 1 1 + e − z \sigma (z) = \frac{1}{1 + e^{-z}} σ(z)=1+e−z1
预测函数: y ^ = a = σ ( W T x ( i ) + b ) \hat{y} =a = \sigma (W^Tx{(i)} + b) y^=a=σ(WTx(i)+b)
激活函数导数: s i g m a ′ ( z ) = σ ( z ) ∗ ( 1 − σ ( z ) ) sigma'(z) = \sigma (z)*(1-\sigma (z)) sigma′(z)=σ(z)∗(1−σ(z))
误差函数导数: E r r o r ′ = ϑ E ϑ a = − y a + 1 − y 1 − a . . . . . . . . . ln ′ x = 1 x Error' = \frac{\vartheta E}{\vartheta a} = -\frac{y}{a} + \frac{1-y}{1-a} ......... \ln 'x = \frac{1}{x} Error′=ϑaϑE=−ay+1−a1−y.........ln′x=x1
线性方程公式:z = W*X
误差函数对权重求偏导数:$ \frac{\vartheta E(w_i, y)}{\vartheta w_i} = (\hat{y} - y)x_i$
更新权重
w 1 : = w 1 − α d w 1 w_1 := w_1 - \alpha dw_1 w1:=w1−αdw1
w 2 : = w 2 − α d w 2 w_2 := w_2 - \alpha dw_2 w2:=w2−αdw2
b : = b − α d b b := b- \alpha db b:=b−αdb
import numpy as np
# Defining the sigmoid function for activations
def sigmoid(x):
return 1 / ( 1 + np.exp(-x) )
# 学习速率
learnrate = 0.5
# Input data
x = np.array([1, 2, 3, 4])
y = np.array(0.5)
print('input data : ', x)
print('output data: ', y)
# Initial weights
w = np.array([0.5, -0.5, 0.3, 0.1])
print('initial weights : ', w)
# 计算输入数据和权重的值
h = np.dot(x, w)
print('线性方程的输出值 : ', h)
# 计算网络输出值
nn_output = sigmoid(h)
print('网络输出 :', nn_output)
# 计算误差项
error_term = nn_output - y
print('error term : ', error_term)
# 更新权重
del_w = learnrate * error_term * x
w -= del_w
print('Neural Network output: ')
print('nn_output : ', nn_output)
print('Amount of Error: ', error_term)
print('change in weights: ', del_w)
print('w ', w)
求梯度流程
- 确定误差函数,平方误差,交叉熵误差
- 对误差函数求权重的偏导,涉及到符合函数求导,求偏导数
- 根据2求得 误差项 error_term
- 更改权重
首先你需要初始化权重。我们希望它们比较小,这样输入在 sigmoid 函数那里可以在接近 0 的位置,而不是最高或者最低处。很重要的一点是要随机地初始化它们,这样它们有不同的初始值,是发散且不对称的。所以我们用一个中心为 0 的正态分布来初始化权重,此正态分布的标准差(scale 参数)最好使用 1 n \frac{1}{\sqrt{n}} n1,其中 n 是输入单元的个数。这样就算是输入单元的数量变多,sigmoid 的输入还能保持比较小。
** 0.5 表示0.5的乘方,也就是根号 n_features表示输入数据的个数,也可以理解为特征数目
weights = np.random.normal(scale=1/n_features**.5, size=n_features)
多层感知器
之前我们研究的是只有一个输出节点网络,代码也很直观。但是现在我们有多个输入单元和多个隐藏单元,它们的权重需要有两个索引 w i j w_{ij} wij, 其中 i 表示输入单元,j 表示隐藏单元。
例如下面这个网络途中,输入单元被标注为
x
1
,
x
2
,
x
3
x_1,x_2, x_3
x1,x2,x3,隐藏层节点是
h
1
a
n
d
h
2
h_1 and \ h_2
h1and h2
代表指向
h
1
h_1
h1的权重被标记为了红色,这样更好区分。
为了定位权重,我们把输入节点的索引 i
和隐藏层节点的索引j
结合,得到:
w
11
w_{11}
w11,
代表从
x
1
到
h
1
x_1到h_1
x1到h1的权重
w
12
w_{12}
w12
代表从
x
1
到
h
2
x_1到h_2
x1到h2的权重
下图包括了从输入层到隐藏层的所有权重,用
w
i
j
w_{ij}
wij表示:
之前我们可以把权重写成数组,用
w
i
w_{i}
wi来索引。现在权重被存储在矩阵
中,用
w
i
j
w_{ij}
wij来索引。矩阵中的每一行
对应从同一输入
节点发出
的权重(行数可以代表输入节点数),每一列
对应传入
同一隐藏
节点的权重(列数代表隐藏层节点数)
对比上面的示意图,确保你了解了不同的权重在矩阵中与在神经网络中的对应关系。
用NumPy 来初始化这些权重,我么需要提供矩阵形状(shape),如果特征是一个包含以下数据的二维数组
# Number of records and input units
# 数据点数量以及每个数据点有多少输入节点
# n_records 代表有多少个输入数据,n_inputs代表每个输入数据有多少个特征
n_records, n_inputs = features.shape
# Number of hidden units
# 隐藏层节点个数
n_hidden = 2
weights_input_to_hidden = np.random.normal(0, n_inputs**-0.5, size=(n_inputs, n_hidden))
这样创建了一个名为 weights_input_to_hidden
的二维数组,维度是 n_inputs
乘 n_hidden
。记住,输入层到隐藏层的节点是所有输入乘以隐藏节点权重的和。所以对每一个隐藏层节点
h
j
h_j
hj,我们需要计算:
h j = ∑ i w i j x i h_j = \sum_{i}w_{ij}x_i hj=i∑wijxi
在这里,我们把输入(一个行向量)与权重相乘。要实现这个,要把输入点乘(内积)以权重矩阵的每一列,例如要计算到第一个隐藏节点 j=1 的输入,你需要把这个输入与权重矩阵的第一列做点乘
h
1
=
x
1
w
11
+
x
2
w
21
+
x
3
w
31
h_1 = x_1w_{11} + x_2w_{21} + x_3w_{31}
h1=x1w11+x2w21+x3w31
针对第二个隐藏节点的输入,你需要计算输入与第二列的点积,以此类推。
在 NumPy 中,你可以直接使用 np.dot
hidden_inputs = np.dot(inputs, weights_input_to_hidden)
你可以定义你的权重矩阵的维度是 n_hidden 乘 n_inputs 然后与列向量形式的输入相乘:
注意:
这里权重的索引在上图中做了改变,与之前图片并不匹配。这是因为,在矩阵标注时行索引永远在列索引之前,所以用之前的方法做标识会引起误导。你只需要了解这跟之前的权重矩阵是一样的,只是做了转换,之前的第一列现在是第一行,之前的第二列现在是第二行。如果用之前的标记,权重矩阵是下面这个样子的:
切记,上面标注方式是不正确
的,这里只是为了让你更清楚这个矩阵如何跟之前神经网络的权重匹配
构建一个列向量
看到上面的介绍,有时你需要一个列向量,尽管NumPy默认的是行向量。你可以用arr.T来对数组进行转置,但是对一维数组来说,转置还是行向量,所以你可以用arr[:, None]来创建一个列向量:不过最好使用arr[:, None]来创建列向量
import numpy as np
a = np.array([1, 2, 3, 4, 5, 6, 7, 8, 9, 0])
print(a)
print(a.T)
print(a[:, None])
你可以创建一个二维数组,然后用arr.T来得到列向量
b = np.array(a, ndmin=2)
print(b)
print(b.T)
实现多层感知器
import numpy as np
def sigmoid(x):
return 1 / (1 + np.exp(-x))
# Network size
N_input = 4
N_hidden = 3
N_output = 2
np.random.seed(42)
#Make same fake data
x = np.random.randn(4)
weights_input_to_hidden = np.random.normal(0, scale=0.1, size=(N_input, N_hidden))
weights_hidden_to_output = np.random.normal(0, scale=0.1, size=(N_hidden, N_output))
# 激活输入
hidden_layer_in = np.dot(x, weights_input_to_hidden)
# 激活输出
hidden_layer_out = sigmoid(hidden_layer_in)
print('Hidden-layer Output:')
print(hidden_layer_out)
output_layer_in = np.dot(hidden_layer_out, weights_hidden_to_output)
output_layer_out = sigmoid(output_layer_in)
print('Output-layer Output:')
print(output_layer_out)
反向传播
如何让错层神经网络学习呢?我们以了解了使用梯度下降来更新权重,反向传播算法则是他的一个延伸。以一个两层神经网络为例,可以使用符合函数求偏导
的方法计算输入层-隐藏层
权重的误差
要使用梯度下降法更新隐藏层的权重,你需要知道各隐藏层节点的误差对最终输出的影响。每层的输出是由两层间的权重决定的,两层之间产生的误差,按权重缩放后在网络中向前传播。既然我们知道输出误差,便可以用权重来反向传播到隐藏层
范例
以一个简单的两层神经网络为例,计算其权重的更新过程。假设该神经网络包含两个输入值,一个隐藏节点和一个输出节点,隐藏层和输出层的激活函数都是 sigmoid,如下图所示。(注意:图底部的节点为输入值,图顶部的
y
^
\hat{y}
y^ 为输出值。输入层不计入层数,所以该结构被称为两层神经网络。)
线上的数字表示权重,圆圈里的数字表示输入,假设我们的试着训练一些二进制数据,目标值为 y = 1,我们从正向传播开始,
δ o \delta^o δo 表示输出误差项
δ h \delta^h δh 表示隐藏层误差项
η \eta η 表示学习速率
计算输入到隐藏层节点
h = ∑ i w i x i = 0.1 × 0.4 − 0.2 × 0.3 = − 0.02 h = \sum_i w_i x_i = 0.1 × 0.4 - 0.2 × 0.3 = -0.02 h=∑iwixi=0.1×0.4−0.2×0.3=−0.02
计算隐藏层节点输出:
a = f ( h ) = s i g m o i d ( − 0.02 ) = 0.495 a = f(h) = sigmoid(-0.02) = 0.495 a=f(h)=sigmoid(−0.02)=0.495
然后将其作为输出节点的输入,该神经网络的输出可表示为
y ^ = f ( W . a ) = s i g m o i d ( 0.1 × 0.495 ) = 0.512 \hat{y} = f(W.a) = sigmoid(0.1 × 0.495) = 0.512 y^=f(W.a)=sigmoid(0.1×0.495)=0.512
基于该神经网络的输出,就可以使用反向传播来更新各层的权重了。sigmoid 函数的导数
f ′ ( W . a ) = f ( W . a ) ( 1 − f ( W . a ) ) f'(W.a) = f(W.a)(1 - f(W.a)) f′(W.a)=f(W.a)(1−f(W.a))
输出节点的误差项(error term)可表示为
δ o = ( y − ( ^ y ) ) f ′ ( W . a ) = ( 1 − 0.512 ) × 0.512 × ( 1 − 0.512 ) = 0.122 \delta^o = (y - \hat(y)) f'(W.a) = (1 - 0.512) × 0.512 × (1 - 0.512) = 0.122 δo=(y−(^y))f′(W.a)=(1−0.512)×0.512×(1−0.512)=0.122
现在我们要通过反向传播来计算隐藏节点的误差项。这里我们把输出节点的误差项与隐藏层到输出层的权重 WW 相乘。隐藏节点的误差项 ,因为该案例只有一个隐藏节点,这就比较简单了
δ h = W δ o f ′ ( h ) = 0.1 × 0.122 × 0.495 × ( 1 − 0.495 ) = 0.003 \delta^h = W\delta^of'(h) = 0.1 ×0.122×0.495×(1−0.495)=0.003 δh=Wδof′(h)=0.1×0.122×0.495×(1−0.495)=0.003
有了误差,就可以计算梯度下降步长了。隐藏层-输出层权重更新步长是学习速率乘以输出节点误差再乘以隐藏节点激活值。
Δ W o = η δ o a = 0.5 × 0.122 × 0.495 = 0.0302 \Delta W^o = \eta \delta^o a = 0.5×0.122×0.495=0.0302 ΔWo=ηδoa=0.5×0.122×0.495=0.0302
然后,输入-隐藏层权重 w i w_i wi是学习速率乘以隐藏节点误差再乘以输入值。
Δ w i = η δ h x i = ( 0.5 × 0.003 × 0.1 , 0.5 × 0.003 × 0.3 ) = ( 0.00015 , 0.00045 ) \Delta w_i = \eta \delta^hx_i = (0.5×0.003×0.1,0.5×0.003×0.3)=(0.00015,0.00045) Δwi=ηδhxi=(0.5×0.003×0.1,0.5×0.003×0.3)=(0.00015,0.00045)
从这个例子中你可以看到 sigmoid 做激活函数的一个缺点。sigmoid 函数导数的最大值是 0.25,因此输出层的误差被减少了至少 75%,隐藏层的误差被减少了至少 93.75%!如果你的神经网络有很多层,使用 sigmoid 激活函数会很快把靠近输入层的权重步长降为很小的值,该问题称作梯度消失。后面的课程中你会学到在这方面表现更好,也被广泛用于最新神经网络中的其它激活函数。
数学推导
现在我们选择误差公式为平方误差,激活函数为 sigmoid 函数
误差函数: E r r o r = 1 2 m ∑ i = 1 m ( y − y ^ ) 2 Error = \frac{1}{2m} \sum_{i=1}^{m}(y -\hat{y})^2 Error=2m1∑i=1m(y−y^)2
激活函数:$ \sigma (z) = \frac{1}{1 + e^{-z}}$
预测函数: y ( i ) ^ = σ ( W T x ( i ) + b ) \hat{y^{(i)}} = \sigma (W^Tx{(i)} + b) y(i)^=σ(WTx(i)+b)
激活函数导数: s i g m a ′ ( z ) = σ ( z ) ∗ ( 1 − σ ( z ) ) sigma'(z) = \sigma (z)*(1-\sigma (z)) sigma′(z)=σ(z)∗(1−σ(z))
误差函数导数: E r r o r ′ = ( y − y ^ ) Error' = (y -\hat{y}) Error′=(y−y^)
线性方程公式:z = W*X
误差函数对权重求偏导数: ϑ E ( w i , y ) ϑ w i = ( y − y ^ ) ( σ ( z ) ∗ ( 1 − σ ( z ) ) ) x i \frac{\vartheta E(w_i, y)}{\vartheta w_i} = (y - \hat{y})( \sigma (z)*(1-\sigma (z)))x_i ϑwiϑE(wi,y)=(y−y^)(σ(z)∗(1−σ(z)))xi
结合上面公式和范例,数学推导流程如下
- h 隐藏节点输入
- a为隐藏节点输出
- y ^ 为 输 出 结 果 \hat{y} 为输出结果 y^为输出结果
- W h W^h Wh 为 输入节点到隐藏层节点的权重
- W o W^o Wo 为隐藏层节点输出层节点的权重
误差函数对 W o W^o Wo求偏导
( y − y ^ ) ( σ ( W . a ) ∗ ( 1 − σ ( W . a ) ) ) ∗ a (y - \hat{y})(\sigma(W.a) * (1 -\sigma(W.a) )) *a (y−y^)(σ(W.a)∗(1−σ(W.a)))∗a
误差函数对 W h W^h Wh求偏导数
( y − y ^ ) ( σ ( W . a ) ∗ ( 1 − σ ( W . a ) ) ) ∗ ( σ ( h ) ∗ ( 1 − σ ( h ) ) ∗ W o x (y - \hat{y})(\sigma(W.a) * (1 -\sigma(W.a) )) *(\sigma(h) * (1 -\sigma(h) )*W^ox (y−y^)(σ(W.a)∗(1−σ(W.a)))∗(σ(h)∗(1−σ(h))∗Wox
代码实现
import numpy as np
def sigmoid(x):
return 1 / (1 + np.exp(-x))
x = np.array([0.5, 0.1, -0.2])
target = 0.5
learnrate = 0.5
weights_input_hidden = np.array([[0.5, -0.6],
[0.1, -0.2],
[0.1, 0.7]])
weights_hiden_output = np.array([0.5, -0.6])
hidden_layer_input = np.dot(x, weights_input_hidden)
hidden_layer_output = sigmoid(hidden_layer_input)
output_layer_in = np.dot(hidden_layer_output, weights_hiden_output)
output = sigmoid(output_layer_in)
error = target - output
output_error_term = error*output*(1 - output)
hidden_error_term = np.dot(output_error_term, weights_hiden_output) * \
hidden_layer_output * (1 - hidden_layer_output)
print(hidden_error_term)
print(hidden_layer_output)
delta_w_h_o = learnrate * output_error_term * hidden_layer_output
delta_w_i_h = learnrate * hidden_error_term * x[:, None]
print(x[:, None])
print('Change in weights for hidden layer to output layer:')
print(delta_w_h_o)
print('Change in weights for input layer to hidden layer:')
print(delta_w_i_h)
接下来我们做个小练习,代码将我的github
- 预测学生录取
- 预测自行车使用情况