在本练习中,将实现神经网络的反向传播算法,并将其应用于手写数字识别任务。我们将通过反向传播算法实现神经网络成本函数和梯度计算的非正则化和正则化版本,还将实现随机权重初始化和使用网络进行预测的方法。
在前面的练习中,实现了神经网络的前馈传播,并使用它以及我们提供的权重来预测手写数字。在本练习中,您将实现反向传播算法来学习神经网络的参数。
在ex4.m
的第一部分,代码将加载数据,并通过调用函数显示数据,将其显示在一个二维图上。
编写代码加载数据,并通过调用函数显示数据。
这与上次练习中使用的数据集相同。在ex3data1.mat
中有5000个训练样例,每个训练样例是一个20像素 $\times $ 20像素的数字灰度图像。每个像素用一个浮点数表示,表示该位置的灰度强度。 20 × 20 20\times 20 20×20 的像素网格被"展开"成一个 400 400 400 维的向量。每个训练样例X
数据矩阵中都变成了一行。给定一个 5000 × 400 5000\times 400 5000×400 的矩阵X
,其中每一行都是一个手写数字图像的训练样例。
在ex4.m
的第二部分是一个 5000 5000 5000 维的向量y
(包含了训练集的标签)。将数字 0 0 0 映射到值 10 10 10,而数字 1 1 1 到 9 9 9 按其自然顺序被标记为 1 1 1 到 9 9 9 。
先导入相关的函数库
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from scipy.io import loadmat
编写加载数据的代码,并且传入参数:数据文件的路径
def load_data(path):
data=loadmat(path)
X=data['X']
y=data['y'].flatten() #将data['y']展开成一维
return X,y
随机绘制100个数字
def plot_images_100(X):
ind=np.random.choice(range(5000),100)#从[0,5000)中任选100个
images=X[ind]
fix,ax_array=plt.subplots(nrows=10,ncols=10,sharex=True,sharey=True,figsize=(8,8)) #绘制的图形为:10*10,并且公用x和y
for r in range(10):
for c in range(10):
ax_array[r,c].matshow(images[r*10+c].reshape(20,20),cmap='gray_r')#每一个图形都是400个数字构成,所以要reshape为 20*20
plt.xticks([])
plt.yticks([])
plt.show()
X,y=load_data('ex4data1.mat')
查看X和y的数据规模以及具体数据
X,y
(array([[0., 0., 0., ..., 0., 0., 0.],
[0., 0., 0., ..., 0., 0., 0.],
[0., 0., 0., ..., 0., 0., 0.],
...,
[0., 0., 0., ..., 0., 0., 0.],
[0., 0., 0., ..., 0., 0., 0.],
[0., 0., 0., ..., 0., 0., 0.]]),
array([10, 10, 10, ..., 9, 9, 9], dtype=uint8))
调用plot_images_100
函数,查看绘制的图形
plot_images_100(X)
神经网络如图所示:它有 3 3 3个层,一个输入层,一个隐藏层和一个输出层。
输入是数字图像的像素值。由于图像的大小是 20 × 20 20\times 20 20×20,这给了 400 400 400 个输入层单位(不包括总是输出 + 1 +1 +1 的额外偏差单位)。训练数据将通过ex4.m
加载到变量X
和y
中。ex4weights.mat
中存储了已经训练过的神经网络参数 θ ( 1 ) , θ ( 2 ) \theta(1),\theta(2) θ(1),θ(2),将它们加载到变量Theta1
和Theta2
中。这些参数是针对一个在第二层有 25 25 25个单元和 10 10 10个输出单元的神经网络(对应于10个数字类)。
获取训练数据集,并且进行相应的处理
raw_X,raw_y=load_data('ex4data1.mat')
X=np.insert(raw_X,0,np.ones(raw_X.shape[0]),axis=1)#在第0列插入全1
X.shape
(5000, 401)
将标签值 ( 1 , 2 , 3 , 4 , . . . , 10 ) (1,2,3,4,...,10) (1,2,3,4,...,10)转化成非线性相关的向量,即 y y y向量对应位置为 1 1 1,比如 y [ 5 ] = 1 y[5]=1 y[5]=1那么 y [ 5 ] = [ 0 , 0 , 0 , 0 , 1 , 0 , 0 , 0 , 0 , 0 ] y[5]=[0,0,0,0,1,0,0,0,0,0] y[5]=[0,0,0,0,1,0,0,0,0,0]。数据中的 y [ i ] = 10 y[i]=10 y[i]=10,就是表示原本的 y [ i ] = 0 y[i]=0 y[i]=0,只是数据提前把 0 0 0转换成了 10 10 10。
# from sklearn.preprocessing import OneHotEncoder #用sklearn中OneHotEncoder函数
# y = np.mat(raw_y).T 转换成[5000,1]的矩阵
# encoder = OneHotEncoder(sparse=False) #sparse:若为True时,返回稀疏矩阵;否则返回数组,默认为True。
# y_onehot = encoder.fit_transform(y)
# y_onehot.shape,y_onehot[0]
def expand_y(y):
result=[]
#将整数y[i]修改为向量,对应下标位置置为1,其余为0
for it in y:
y_array=np.zeros(10)
y_array[it-1]=1
result.append(y_array)
return np.array(result)
y = expand_y(raw_y)
y.shape,y[0]
((5000, 10), array([0., 0., 0., 0., 0., 0., 0., 0., 0., 1.]))
ex4weights.mat
中存储了已经训练过的神经网络参数 θ ( 1 ) , θ ( 2 ) \theta(1),\theta(2) θ(1),θ(2),将它们加载到变量Theta1
和Theta2
中。这些参数是针对一个在第二层有 25 25 25个单元和 10 10 10个输出单元的神经网络(对应于10个数字类)。 θ 1 \theta1 θ1的规模为 25 ∗ 401 25*401 25∗401, θ 2 \theta2 θ2的规模为 10 ∗ 26 10*26 10∗26。
def load_weight(path):
data=loadmat(path)
return data['Theta1'],data['Theta2']
Theta1,Theta2=load_weight('ex4weights.mat')
Theta1.shape,Theta2.shape
((25, 401), (10, 26))
当使用高级优化方法来优化神经网络时,需要将多个参数矩阵展开,才能传入优化函数,然后再恢复形状。
def serialize(t1,t2):
#参数展开
return np.concatenate((t1.flatten(),t2.flatten()))#等同于 np.r_[t1.flatten(),t2.flatten()]
theta=serialize(Theta1, Theta2) #扁平化参数
theta.shape
(10285,)
def deserialize(seq):
#提取参数
return seq[:25*401].reshape(25,401),seq[25*401:].reshape(10,26)
def sigmoid(z):
return 1/(1+np.exp(-z))
结合下图实现feed_forward
函数
θ 1 \theta1 θ1的规模为 25 × 401 25\times 401 25×401, θ 2 \theta2 θ2的规模为 10 × 26 10\times 26 10×26, a 1 a_1 a1的规模为 5000 × 401 5000\times 401 5000×401。
def feed_forward(theta,X):
Theta1,Theta2=deserialize(theta)
a1=X
z2=a1@Theta1.T
a2=sigmoid(z2)
a2=np.insert(a2,0,1,axis=1)
z3=a2@Theta2.T
a3=sigmoid(z3)
return a1,z2,a2,z3,a3
现在将实现神经网络的成本函数和梯度。神经网络的代价函数(没有正则化)是:
k = 10 k=10 k=10是可能的标签的总数。原始的标签(在变量y
中)是 1 , 2 , . . . , 10 1,2,...,10 1,2,...,10,为了训练一个神经网络,需要将标签重新编码为只包含值 0 0 0或 1 1 1的向量。
举个例子:如果 x ( i ) x^{(i)} x(i)是 5 5 5的图像,那么y
则是一个 y [ 5 ] = 1 y[5]=1 y[5]=1的 10 10 10维向量,其他元素等于 0 0 0。
要求实现前馈计算,即为每个训练数据 i i i计算 h θ ( x ( i ) ) h_{\theta}(x^{(i)}) hθ(x(i))并计算所有训练数据的成本。正确的最终成本约为 0.287629 0.287629 0.287629。
输出层输出的是对训练样本的预测,包含 5000 5000 5000个数据,每个数据对应了一个包含 10 10 10个元素的向量,代表了结果有 10 10 10类。在公式中,每个元素与log项对应相乘。
构造Cost
函数:使用提供训练好的参数θ,算出的cost应该为 0.287629 0.287629 0.287629。
def Cost(theta,X,y):
a1,z2,a2,z3,h=feed_forward(theta,X)
J=0
for i in range(X.shape[0]):
ans=np.multiply(-y[i],np.log(h[i]))-np.multiply((1-y[i]),np.log(1-h[i]))
sum=np.sum(ans)
J+=sum
J=J/(X.shape[0])
return J
Cost(theta, X, y)
0.2876291651613188
正则化神经网络的代价函数:
假设神经网络将只有 3 3 3个层:一个输入层,一个隐藏层和一个输出层。但是,设计的代码应该适用于任意数量的输入单元、隐藏单元和输出单元。不应该规范与偏差相对应的项。
使用之前的Theta1
和Theta2
以及 λ = 1 \lambda=1 λ=1参数来调用Regularized_cost
函数,正确的最终答案大约为 0.383770 0.383770 0.383770。(注意不要将每层的偏置项正则化)
def Regularized_cost(theta,X,y,l=1):
J=Cost(theta, X, y)+l/(2*X.shape[0])*(np.sum(np.power(Theta1[:,1:],2))+np.sum(np.power(Theta2[:,1:],2)))
return J
Regularized_cost(theta, X, y,1)
0.38376985909092354
在本部分的练习中,将实现反向传播算法来计算神经网络代价函数的梯度。一旦计算了梯度,将通过使用高级优化器最小化代价函数 J ( Θ ) J(Θ) J(Θ)来训练神经网络。
首先实现反向传播算法来计算(非正则化)神经网络的参数的梯度。验证了在非正则化情况下的梯度计算是正确的之后,再实现正则化神经网络的梯度。
s s s型函数的梯度:
def sigmoid_gradient(z):
return sigmoid(z)*(1-sigmoid(z))
在训练神经网络时,随机初始化参数是很重要的,可以打破数据的对称性。一个有效的策略是在均匀分布 ( − ϵ , ϵ ) (−\epsilon,\epsilon) (−ϵ,ϵ)中随机选择值,选择 ϵ = 0.12 \epsilon = 0.12 ϵ=0.12这个范围的值来确保参数足够小,使得训练更有效率。
因此接下来需要完成随机初始化权重,初始化 Θ \Theta Θ的权重
def random_init(size):
#np.random.uniform():随机生成指定范围的浮点数,从一个均匀分布[low,high)中随机采样,定义域是左闭右开,包含low,不包含high,ndarray类型,其形状与size中描述一致.
return np.random.uniform(-0.12,0.12,size)
给定一个训练数据 ( x ( t ) , y ( t ) ) (x^{(t)},y^{(t)}) (x(t),y(t)),将先运行一个向前传递来计算整个网络中的所有激活值,包括假设的输出值 h Θ ( x ) h_{\Theta}(x) hΘ(x),然后,对于第 l l l层中的每个节点 j j j,需要计算误差项 δ j ( l ) \delta_{j}^{(l)} δj(l)。
对于一个输出节点,可以直接计算神经网络激活值和真实目标值之间的差异,并使用它来定义 δ j ( 3 ) \delta_{j}^{(3)} δj(3)(本题中第 3 3 3层是输出层)。对于隐藏单元,根据第 ( l + 1 ) (l+1) (l+1)层中节点的误差项的加权平均值来计算 δ j ( l ) \delta_{j}^{(l)} δj(l)。
实现一个for
循环 t = 1 : m t=1:m t=1:m,并将步骤 1 ∼ 4 1\sim4 1∼4放在for
循环中,第 t t t次循环计算第 t t t个训练数据 ( x ( t ) , y ( t ) ) (x^{(t)},y^{(t)}) (x(t),y(t))的相关数据。步骤 5 5 5将累积的梯度除以 m m m,得到神经网络代价函数的梯度。
1.将第 t t t个训练数据 x ( t ) x^{(t)} x(t)设置为输入层的值 ( a ( 1 ) ) (a^{(1)}) (a(1))。执行前馈传递(feedforward
),计算第 2 2 2层和第 3 3 3层的激活量 ( z ( 2 ) 、 a ( 2 ) 、 z ( 3 ) 、 a ( 3 ) ) (z^{(2)}、a^{(2)}、z^{(3)}、a^{(3)}) (z(2)、a(2)、z(3)、a(3))。注意,需要添加一个 + 1 +1 +1项,以确保第 1 1 1层和第 2 2 2层的激活向量也包括偏置单元。
2.对于第 3 3 3层(输出层)中的每个输出单元 k k k,设定 δ k ( 3 ) = ( a k ( 3 ) − y k ) ( y k ∈ { 0 , 1 } ) \delta_{k}^{(3)}=(a_{k}^{(3)}-y_k)(y_k\in \lbrace 0,1 \rbrace ) δk(3)=(ak(3)−yk)(yk∈{0,1}), y k y_k yk表示当前的训练数据是否属于第 k k k类,如果属于第 k k k类则 y k = 1 y_k=1 yk=1,否则 y k = 0 y_k=0 yk=0。
3.对于隐藏层 l = 2 l=2 l=2,设置 δ ( 2 ) = ( Θ ( 2 ) ) T δ ( 3 ) . ∗ g ′ ( z ( 2 ) ) \delta^{(2)}=(\Theta^{(2)})^{T}\delta^{(3)}.*g'(z^{(2)}) δ(2)=(Θ(2))Tδ(3).∗g′(z(2))。
4.使用以下公式累积此训练数据中的梯度。注意,应该跳过或删除 δ 0 ( 2 ) \delta_{0}^{(2)} δ0(2)。那么 Δ ( l ) = Δ ( l ) + δ ( l + 1 ) ( a ( l ) ) T \Delta^{(l)}=\Delta^{(l)}+\delta^{(l+1)}(a^{(l)})^T Δ(l)=Δ(l)+δ(l+1)(a(l))T。
5.通过将累积的梯度除以 m m m,得到神经网络代价函数的(非正则化的)梯度。
def Gradient(theta,X,y):
Theta1,Theta2=deserialize(theta)
#step 1:
a1,z2,a2,z3,h=feed_forward(theta, X) #a2.shape:(5000, 26)
#step 2:
d3=h-y #d3.shape:(5000,10)
#step 3:
d2=np.multiply(d3@Theta2[:,1:],sigmoid_gradient(z2)) #d2.shape:(5000, 25)
#step 4:
D2=d3.T@a2 #D2.shape:(10, 26)
D1=d2.T@a1 #D1.shape:(25, 401)
#step 5:
D=np.multiply(1/(X.shape[0]),serialize(D1, D2)) #D.shape:(10285,)
return D
在神经网络中,需要最小化成本函数 J ( Θ ) J(\Theta) J(Θ)。为了对参数执行梯度检查,考虑将参数 Θ ( 1 ) , Θ ( 2 ) \Theta^{(1)},\Theta^{(2)} Θ(1),Θ(2)"展开"到一个向量 θ \theta θ中。通过这样做,可以认为成本函数是 J ( Θ ) J(\Theta) J(Θ),并使用以下梯度检查过程:
假设有一个函数 f i ( θ ) f_i(\theta) fi(θ),可以计算 ∂ ∂ θ i J ( θ ) \frac{\partial}{\partial\theta_i}J(\theta) ∂θi∂J(θ);检查一下 f i ( θ ) f_i(\theta) fi(θ)是否输出了正确的导数值:
因此, θ ( i + ) θ^{(i+)} θ(i+)与 θ \theta θ基本相同,除了它的第 i i i个元素被增加了 ϵ \epsilon ϵ; θ ( i − ) θ^{(i-)} θ(i−)与 θ \theta θ基本相同,除了它的第 i i i个元素被减少了 ϵ \epsilon ϵ。现在通过检查每个 i i i来数值验证 f i ( θ ) f_i(\theta) fi(θ)的正确性:
这两个值相互近似的程度将取决于 J J J。但是假设 ϵ = 1 0 − 4 \epsilon=10^{-4} ϵ=10−4 ,通常会发现上面式子的左极限和右极限至少会存在4位有效的数字(通常是更多的)。如果反向传播实现是正确的,相对误差应该低于 1 0 − 9 10^{-9} 10−9
def Gradient_checking(theta,X,y,e):
def A_numeric_grad(right,left):
return (Regularized_cost(right, X, y)-Regularized_cost(left, X, y))/(2*e)
pass
numeric_grad=[]
right=theta.copy()
left=theta.copy()
for i in range(len(theta)):
if(i!=0):
right[i-1]=right[i-1]-e
left[i-1]=left[i-1]+e
right[i]=right[i]+e
left[i]=left[i]-e
numeric_grad.append(A_numeric_grad(right, left))
numeric_grad=np.array(numeric_grad)
analytic_grad=Regularized_cost(theta, X, y)
diff=np.linalg.norm(numeric_grad-analytic_grad)/np.linalg.norm(numeric_grad+analytic_grad)
print('如果反向传播实现是正确的,相对误差应该低于1e-9.\n相对误差: {}\n'.format(diff))
Gradient_checking(theta, X, y, 0.0001)#这个运行真的慢!!! 运行似乎有问题,之后改
成功实现反向传播算法后,向梯度添加正则化。为了考虑正则化,可以在使用反向传播计算梯度后将其作为附加项添加。
具体来说,在使用反向传播计算了 Δ i j ( l ) \Delta_{ij}^{(l)} Δij(l)之后,可以使用来添加正则化:
注意,不要正则化偏差项 Θ ( l ) \Theta^{(l)} Θ(l)的第一列。注意,在参数 Θ i j ( l ) \Theta_{ij}^{(l)} Θij(l)中,下标 i ∈ [ 1 , ] i\in[1,] i∈[1,],下标 j ∈ [ 0 , ] j\in[0,] j∈[0,],如图:
def Regularized_gradient(theta,X,y,l=1):
a1,z2,a2,z3,h=feed_forward(theta, X) #a2.shape:(5000, 26)
D1,D2=deserialize(Gradient(theta, X, y))
Theta1,Theta2=deserialize(theta)
Theta1[:,0]=0
Theta2[:,0]=0
reg_D1=D1+(l/X.shape[0])*Theta1
reg_D2=D2+(l/X.shape[0])*Theta2
return serialize(reg_D1, reg_D2)
在成功实现了神经网络代价函数和梯度计算之后,使用fmincg
来学习得到更优秀的集合参数。如果实验过程正确,那么训练准确率约为95.3%(由于随机初始化,误差约1%)。通过训练神经网络进行迭代,可以获得更高的训练精度。也可以考虑改变正则化参数 λ \lambda λ来得到更高的训练精度。更加正确的学习设置,就有可能让神经网络更完美地匹配训练集。
import scipy.optimize as opt
def training(X,y):
theta=random_init(10285)
res=opt.minimize(fun=Regularized_cost,x0=theta,args=(X,y,1),method='TNC',jac=Regularized_gradient,options={'maxiter':400})
return res
res=training(X, y)
res
fun: 0.2289451433747141
jac: array([ 1.92570206e-04, 5.03964124e-07, -1.40295013e-06, ...,
-5.59838186e-05, -7.13012121e-06, -1.57255878e-04])
message: 'Converged (|f_n-f_(n-1)| ~= 0)'
nfev: 397
nit: 23
status: 1
success: True
x: array([-0.43822395, 0.00251982, -0.00701475, ..., -0.52977546,
-0.5889591 , 1.83802919])
from sklearn.metrics import classification_report
def accuracy(theta,X,y):
a1,z2,a2,z3,h=feed_forward(theta, X) #a2.shape:(5000, 26)
y_pred=np.argmax(h,axis=1)+1
y.shape,y_pred
print(classification_report(y, y_pred))
accuracy(res.x, X, raw_y)
precision recall f1-score support
1 0.99 1.00 0.99 500
2 0.99 0.99 0.99 500
3 0.99 0.99 0.99 500
4 1.00 0.99 0.99 500
5 1.00 1.00 1.00 500
6 1.00 1.00 1.00 500
7 0.99 0.99 0.99 500
8 1.00 1.00 1.00 500
9 0.99 0.99 0.99 500
10 0.99 1.00 1.00 500
accuracy 0.99 5000
macro avg 0.99 0.99 0.99 5000
weighted avg 0.99 0.99 0.99 5000
理解神经网络正在学习什么可以考虑将隐藏单元所捕获的表征可视化。即给定一个特定的隐藏单元,找到一个输入X
,并且将它激活(即有一个激活值 ( a i ( l ) ) (a^{(l)}_i) (ai(l))接近 1 1 1)。对于训练过的神经网络, Θ ( 1 ) \Theta^{(1)} Θ(1)中每一行都是一个 401 401 401维的向量,代表每个隐藏层单元的参数。如果忽略偏置项,就能得到 400 400 400维的向量,这个向量代表每个样本输入到每个隐层单元的像素的权重。因此可视化的其中一种方法是,reshape
这个 400 400 400维的向量为 ( 20 , 20 ) (20,20) (20,20)的图像然后显示出来。
接下来通过使用显示数据功能,它将显示一个包含 25 个 25个 25个单元的图像(如下图),每个单元对应于网络中的一个隐藏单元:
在经过训练的神经网络中,会发现隐藏的单元大致对应于在输入中寻找笔画和其他模式的检测器。
def plot_hidden(theta):
Theta1,Theta2=deserialize(theta)
Theta1=Theta1[:,1:]
fix,ax_array=plt.subplots(nrows=5,ncols=5,sharex=True,sharey=True,figsize=(8,8))
for r in range(5):
for c in range(5):
ax_array[r,c].matshow(Theta1[r*5+c].reshape(20,20),cmap='gray_r')
plt.xticks([])
plt.yticks([])
plt.show()
plot_hidden(theta)