实现反向传播 backpropagation神经网络算法, 对图片中手写数字 0-9 进行识别。
手写数字图片数据。每张图片20px * 20px,也就是一共400个特征。
1 样本数据 ex4data1.mat
一共有5000个训练实例(training instance)。
- 用 X 矩阵表示整个训练集,则 X 是一个 5000*400 (5000行 400列)的矩阵
- 用 y 向量标记整个训练集的结果,则 y 是一个 5000*1 (5000行 1列)的列向量
2 模型建立
上图所示神经网络一共有三个层,分别是输入层(input layer),隐藏层(hidden layer),输出层(output layer)。特点如下:
1、每层由单元(units)组成
2、输入层是有训练集的实例特征向量传入
3、经过连接接点的权重(weight)传入下一层,一层的输出是下一层的输入
4、隐藏层的个数可以是任意的,输入层有一层,输出层有一层
5、每个单元也可以称之为神经结点,根据生物学来源定义
6、以上成为两层的神经网络,输入层是不算在里面的
7、一层中加权求和,然后根据非线性方程转化输出
8、作为多层向前神经网络,理论上,如果有足够的隐藏层,和足够的训练集,可以模拟出任何方程
3 完整 Python代码
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from scipy.io import loadmat## 1 加载和可视化数据
# 1.1 加载数据
print('第一步 加载和可视化数据 ...')
print('1.1 加载数据 ...')
data = loadmat('ex4data1.mat')
X = data['X']
y = data['y']
print(f'输入特征的形状 X.shape = {X.shape}, 输出特征的形状 y.shape = {y.shape}')# 1.2 可视化X中部分数据(随机选择9个图片)
print('1.2 可视化数据 ...')
fig,axs=plt.subplots(3,3)
axs=axs[:,:].flatten()
import random
for i in range(len(axs)):
j=random.randint(0,len(X)-1)
num=int(np.sqrt(len(X[0,:])))
axs[i].imshow(X[j,:].reshape(num,num).T)
axs[i].set_ylabel(str(y[j]))
plt.show()# 1.3 对y标签进行一次one-hot编码。
# one-hot编码将类标签n (k类)转换为长度为k的向量,
# 其中索引n为‘hot’(1),二其余为0。
# Scikitlearn有一个内置的使用程序,我们可以使用这个。
print('1.3 对y标签进行一次one-hot编码 ...')from sklearn.preprocessing import OneHotEncoder
encoder = OneHotEncoder(sparse = False)
y_onehot = encoder.fit_transform(y)
print(f'输出特征的形状 y_onehot.shape = {y_onehot.shape}')## 2 构建的神经网络
## 1个输入层:与实例数据(400 + 偏置单元)大小匹配
## 1个隐藏层:25个单位的隐藏层(带有偏置单元的26个),
## 1个输出层:10个单位对应我们的一个one-hot编码类标签。# 2.1 前向传播
# 2.1.1 定义sigmoid函数
def sigmoid(z):
return 1 / (1 + np.exp(-z))# 2.1.2 定义前向传播函数 (400 + 1) -> (25 + 1) -> (10)
def forward_propagate(X, theta1, theta2):
# INPUT:参数值theta,数据X
# OUTPUT:当前参数值下前项传播结果
# TODO:根据参数和输入的数据计算前项传播结果
# STEP1:获取样本个数
m = X.shape[0]
# STEP2:实现神经网络正向传播
a1 = np.insert(X, 0, values = np.ones(m), axis = 1) # 给X矩阵插入一1列元素
z2 = a1 * theta1.T
a2 = sigmoid(z2)
a2 = np.insert(a2, 0, values = np.ones(m), axis = 1) # 注意插入1列元素
z3 = a2 * theta2.T
h = sigmoid(z3)
return a1, z2, a2, z3, h
# 2.1.3 定义前向传播的代价函数
def cost(params, input_size, hidden_size, num_labels, X, y, lamda):
'''
输入参数:
params:神经网络参数
input_size:输入层维度
hidden_size:隐藏层维度
num_labels:输出标签维度
X,y:训练数据及标签
lamda:正则化参数
返回值:当前参数值params下的代价函数
'''
# STEP1:获取样本个数
m = X.shape[0]
# STEP2:将矩阵X,y转换为numpy型矩阵
X = np.matrix(X)
y = np.matrix(y)
# STEP3:从params中获取神经网络参数,并按照输入层维度和隐藏层维度重新定义参数的维度
theta1 = np.matrix(np.reshape(params[:hidden_size * (input_size + 1)], (hidden_size, (input_size + 1))))
theta2 = np.matrix(np.reshape(params[hidden_size * (input_size + 1):], (num_labels, (hidden_size + 1))))
# STEP4:调用前面写好的前项传播函数
a1, z2, a2, z3, h = forward_propagate(X, theta1, theta2)
# STEP5:初始化代价函数
J = 0
# STEP6:根据公式计算代价函数
for i in range(m): # 遍历每个样本
first_term = np.multiply(-y[i,:], np.log(h[i,:]))
second_trem = np.multiply((1 - y[i,:]), np.log(1 - h[i,:]))
J += np.sum(first_term - second_trem)
J = J / m;
# STEP7:计算代价函数的正则化部分
J += (float(lamda) / (2 * m)) * (np.sum(np.power(theta1[:,1:], 2)) + np.sum(np.power(theta2[:,1:], 2)))
return J# 2.1.4 初始化设置
input_size = 400; # 20x20=400像素 每个像素为一个特征,输入层有400个特征
hidden_size = 25; # 隐藏层有25个神经元
num_lables = 10; # 输出层有10 标记, 从1 到 10
# (注意我们将 "0" 映射为 标记 10)
lamda = 1 # 正则化参数print(f'第二步 定义二层神经网络....[400,25,10]')
print(f'2.1 随机初始化完整网络参数大小的参数数据.....')# 2.1.4.1 随机初始化完整网络参数大小的参数数据
params = (np.random.random(size = hidden_size * (input_size + 1) + num_lables * (hidden_size + 1)) - 0.5) * 0.25m = X.shape[0] # 样本个数
X = np.matrix(X) # 将输入特征数组转化为矩阵
y = np.matrix(y) # 将输出特征数组转换为矩阵# 2.1.4.2 将参数数据params解开为每个层的参数矩阵
theta1 = np.matrix(np.reshape(params[:hidden_size * (input_size + 1)], (hidden_size, (input_size + 1))))
theta2 = np.matrix(np.reshape(params[hidden_size * (input_size + 1):], (num_lables, (hidden_size + 1))))
print(f'theta1.shape = {theta1.shape}, theta2.shape = {theta2.shape}')# 2.1.5 前向传播
print(f'2.2 前向传播.....')
a1, z2, a2, z3, h = forward_propagate(X, theta1, theta2)
print(f'a1.shape = {a1.shape}, z2.shape = {z2.shape}, ')
print(f'a2.shape = {a2.shape}, z3.shape = {z3.shape}, ')
print(f' h.shape = {h.shape}。')# 2.1.6 计算代价函数
J = cost(params, input_size, hidden_size, num_lables, X, y_onehot, lamda)
print(f'代价函数 J = {J}。')## 2.2 反向传播法
## 反向传播参数更新计算将减少训练数据上的网络误差。
## 我们需要的第一件事是计算我们之前创建的Sigmiod函数的梯度函数。# 2.2.1 定义Sigmoid函数的梯度函数
def sigmoid_gradient(z):
return np.multiply(sigmoid(z), (1 - sigmoid(z)))# 2.2.2 定义反向传播函数
# 由于反向传播所需要的计算是代价函数中所需的计算过程,
# 我们实际上将扩展代价函数以执行反向传播并返回代价和梯度。
def backward_propagate(params, input_size, hidden_size, num_labels, X, y, lamda):
'''
输入参数:
params:神经网络参数
input_size:输入层维度
hidden_size:隐藏层维度
num_labels:输出标签维度
X,y:训练数据及标签
lamda:正则化参数
返回值:
J -- 当前参数值params下的代价函数
grade -- 反向传播梯度值
'''
# STEP1:获取样本个数
m = X.shape[0]
# STEP2:将矩阵X,y转换为numpy型矩阵
X = np.matrix(X)
y = np.matrix(y)
# STEP3:从params中获取神经网络参数,并按照输入层维度和隐藏层维度重新定义参数的维度
theta1 = np.matrix(np.reshape(params[:hidden_size * (input_size + 1)], (hidden_size, (input_size + 1))))
theta2 = np.matrix(np.reshape(params[hidden_size * (input_size + 1):], (num_lables, (hidden_size + 1))))# STEP4:调用前面写好的前项传播函数
a1, z2, a2, z3, h = forward_propagate(X, theta1, theta2)
# STEP5:初始化
J = 0
theta1_grad = np.zeros(theta1.shape) # (25, 401)
theta2_grad = np.zeros(theta2.shape) # (10, 26)
# STEP6:计算代价函数(调用函数)
for i in range(m): # 遍历每个样本
first_term = np.multiply(-y[i,:], np.log(h[i,:]))
second_trem = np.multiply((1 - y[i,:]), np.log(1 - h[i,:]))
J += np.sum(first_term - second_trem)
J = J / m;
# STEP7:实现反向传播(这里用到的公式请参考原版作业PDF的第5页)
for t in range(m): # 遍历每个样本
a1t = a1[t,:] # (1, 401)
z2t = z2[t,:] # (1, 25)
a2t = a2[t,:] # (1, 26)
ht = h[t,:] # (1, 10)
yt = y[t,:] # (1, 10)
delta3 = ht - yt
z2t = np.insert(z2t, 0, values = np.ones(1)) # (1, 26)
delta2 = np.multiply((theta2.T * delta3.T).T, sigmoid_gradient(z2t)) # (1, 26)
theta1_grad = theta1_grad + (delta2[:,1:]).T * a1t
theta2_grad = theta2_grad + delta3.T * a2t
# STEP8:加入正则化
theta1_grad[:,1:] = theta1_grad[:,1:] + (theta1[:,1:] * lamda) / m
theta2_grad[:,1:] = theta2_grad[:,1:] + (theta2[:,1:] * lamda) / m
# STEP9:将梯度矩阵转换为单个数据
grad = np.concatenate((np.ravel(theta1_grad), np.ravel(theta2_grad))) # ravel()降维
return J, grad
## 反向传播计算的最难的部分(除了理解为什么我们正在做所有这些计算)是获得正确矩阵维度。
## 顺便说一下,你容易混淆了A * B与np.multiply(A, B)使用。基本上前者是矩阵乘法,
## 后者是元素乘法(除非A或B是标量值,在这种情况下没关系)。
## 无论如何,让我们测试一下,以确保函数返回我们期望的。# 2.2.3 反向传播
J, grad = backward_propagate(params, input_size, hidden_size, num_lables, X, y_onehot, lamda)
print('2.3 反向传播')
print(f'代价函数 J.shape = {J.shape}, grad.shape = {grad.shape}。')## 2.4 开始训练网络
## 由于目标函数不太可能完全收敛,我们对迭代次数进行了限制。
## 我们的总代价已经下降到0.5以下,这是算法正常工作的一个很好的指标。
## 让我们使用它发现的参数,并通过网络转发,以获得一些预测。
from scipy.optimize import minimize
print('2.4 训练网络....')
fmin = minimize(fun = backward_propagate,
x0 = params,
args = (input_size, hidden_size, num_lables, X, y_onehot, lamda),
method = 'TNC',
jac = True,
options = {'maxiter':2500})
print(f'迭代100次后的最优参数 fmin: \n {fmin}')## 2.5 预测
X = np.matrix(X)
theta1 = np.matrix(np.reshape(fmin.x[:hidden_size * (input_size + 1)], (hidden_size, (input_size + 1))))
theta2 = np.matrix(np.reshape(fmin.x[hidden_size * (input_size + 1):], (num_lables, (hidden_size + 1))))## 2.5.1 预测全部样本的准确率
a1, z2, a2, z3, h = forward_propagate(X, theta1, theta2)
y_pred = np.array(np.argmax(h, axis = 1) + 1)
acc=np.mean(y_pred==y)
print(f'整个样本的预测准确率 = {acc*100}%') #打印预测准确率
运行后结果如下: