识别猫的项目
- 神经网络训练步骤
- 数据准备
- 定义神经网络结构
- 解析神经网络、初始化参数(w、b)
- 输入数据,前向传播
- 得到本轮迭代的损失值
- 求最末层误差,反向传播计算各层梯度
- 根据各层的w、b梯度,使用梯度下降更新一次参数
- 重复以上步骤,直到损失值低于设定阈值,模型收敛
- 神经网络的探索
- 梯度检验,检测反向传播是否有问题
- 探索各种权重初始化
- 探索各种优化算法
- 批量归一化
神经网络训练步骤
数据准备
import h5py # 数据是 H5 文件,需要 h5 模块
import matplotlib.pyplot as plt # 绘图库,之后会查看图片
import numpy as np
train_dataset = h5py.File('./我的数据集/train_catvnoncat.h5','r') # 读取训练集(创建了一个字典格式的对象)
test_dataset = h5py.File('./我的数据集/test_catvnoncat.h5','r') # 读取测试集(创建了一个字典格式的对象)
# 因为对象是字典,所以用数据之前,还需要键名
for key1 in train_dataset.keys():
print( train_dataset[key1].name ) # 查看训练集键名(标签类别list_classes、图片特征train_set_x、图片标签train_set_y)
for key2 in test_dataset.keys():
print( test_dataset[key2].name ) # 查看测试集键名(标签类别list_classes、图片特征test_set_x、图片标签test_set_y)
train_set_x = np.array(train_dataset["train_set_x"][:]) # 通过键名,访问所有数据,并保存到数组里
train_set_y = np.array(train_dataset["train_set_y"][:]) # 通过键名,访问所有数据,并保存到数组里
test_set_x = np.array(test_dataset["test_set_x"][:]) # 通过键名,访问所有数据,并保存到数组里
test_set_y = np.array(test_dataset["test_set_y"][:]) # 通过键名,访问所有数据,并保存到数组里
print("train_set_x.shape = ",train_set_x.shape) # 查看训练集数组维度( (209, 64, 64, 3) 意思是,209张图片,尺寸是 64*64*3,64x64x3 = 12288个特征 )
print("test_set_x.shape = ",test_set_x.shape) # 查看测试集数组维度( (50, 64, 64, 3) 意思是,50张图片,尺寸是 64*64*3,64x64x3 = 12288个特征 )
一张图片是一个三维数组 64 * 64 * 3
,64 是长和宽,3 是 RGB 三个通道,因为这是一张彩色图片,每个像素点必须用三个数来代表,一张图片就要用 1 万多个数来描述。
209 张图片,是一个四维数组 209 * 64 * 64 * 3
,而神经元的输入是二维的列矩阵 209 * 1
,所以我们要转换一下。
# 图片特征 xx_set_x:(209, 64, 64, 3) -> (12288,209)
train_set_x = train_set_x.reshape(train_set_x.shape[0], -1).T # (209, 64, 64, 3) 变成 (12288,209)
test_set_x = test_set_x.reshape(test_set_x.shape[0], -1).T # (209, 64, 64, 3) 变成 (12288,50)
# 图片标签 xx_set_y:(209,) -> (1,209)
train_set_y = train_set_y.reshape(1,train_set_y.shape[0]) # (209,) 变成 (1,209)
test_set_y = test_set_y.reshape(1,test_set_y.shape[0]) # (50,) 变成 (1,50)
# P.S. 标签类别list_classes 非0即1,0即非猫,1即是猫。
最后归一化,我们要把数据经过处理后使之限定在一定的范围内。比如通常限制在区间 [0, 1]
。
train_set_x = train_set_x/255.0
test_set_x = test_set_x/255.0
# 除以255.0是为了使数据的取值范围在sigmoid激励函数的取值范围内
#【注:灰度图像素取值为0-255,相除后取值在0-1之间,符合激励函数的输出范围】
好,现在我们把数据准备这部分封装为一个函数。
import h5py # 数据是 H5 文件,需要 h5 模块
import matplotlib.pyplot as plt # 绘图库,之后会查看图片
import numpy as np
def load_dataset():
# 创建文件对象
train_dataset = h5py.File('./我的数据集/train_catvnoncat.h5','r')
test_dataset = h5py.File('./我的数据集/test_catvnoncat.h5','r')
# 读取数据
train_set_x = np.array(train_dataset["train_set_x"][:])
train_set_y = np.array(train_dataset["train_set_y"][:])
test_set_x = np.array(test_dataset["test_set_x"][:])
test_set_y = np.array(test_dataset["test_set_y"][:])
# 查看第 110 张图片
plt.figure(figsize=(2,2))
plt.imshow(train_set_x[110])
plt.show()
# 变化维度以适应神经网络输入
train_set_x = train_set_x.reshape(train_set_x.shape[0],-1).T
test_set_x = test_set_x.reshape(test_set_x.shape[0],-1).T
train_set_y = train_set_y.reshape(1,train_set_y.shape[0])
test_set_y = test_set_y.reshape(1,test_set_y.shape[0])
# 数据归一化
train_set_x = train_set_x/255.0
test_set_x = test_set_x/255.0
return train_set_x,train_set_y,test_set_x,test_set_y
if __name__ == '__main__':
train_set_x, train_set_y, test_set_x, test_set_y = load_dataset()
# 加载数据
定义神经网络结构
加载数据后,定义神经网络结构。
if __name__ == '__main__':
load_dataset()
# 加载数据
fc_net = [12288, 4, 3, 2, 1]
# 用数组定义神经网络结构,输入层(12288个神经元)->第1层->第2层->第3层->第4层->a,有多少层、一层多少神经元,都取决于工程师的直觉,差不多就行
# 输出值a,a>0.5 ? 1:0
解析神经网络、初始化参数(w、b)
定义神经网络结构之后,解析神经网络
if __name__ == '__main__':
train_set_x, train_set_y, test_set_x, test_set_y = load_dataset()
# 加载数据
fc_net = [12288, 4, 3, 2, 1]
# 用数组定义神经网络结构,输入层(12288个神经元)->第1层->第2层->第3层->第4层->a
# 输出值a,a>0.5 ? 1:0
parameters = init_parameters(fc_net)
# 解析神经网络,初始化权值
现在我们来实现初始化权值 init_parameters
的实现。
# 初始化参数
def init_parameters(fc_net):
parameters = {}
# 定义一个字典,存放参数矩阵W1,b1,W2,b2,W3,b3,W4,b4
Layer_num = len(fc_net)
# 神经网络的层数,通过获取数组长度可得
for L in range(1, Layer_num): # 遍历层数,从第一层开始,但忽略输入层(第 0 层)
parameters["W"+str(L)] = np.random.randn(fc_net[L], fc_net[L-1])*0.01
# 从标准正态分布中(取值范围[-3,3])选取值,为方便激活函数(取值范围[-1,1]),乘以0.01后,再赋值给创建的键值对(W1、W2、W3···)
parameters["b"+str(L)] = np.zeros((fc_net[L], 1))
for L in range(1, Layer_num):
print("W"+str(L)+" = ", parameters["W"+str(L)].shape)
print("b"+str(L)+" = ", parameters["b"+str(L)].shape)
return parameters
输入数据,前向传播
if __name__ == '__main__':
train_set_x, train_set_y, test_set_x, test_set_y = load_dataset()
# 加载数据
fc_net = [12288, 4, 3, 2, 1]
# 输入层->第1层->第2层->第3层->第4层->a
# 输出值a,a>0.5 ? 1:0
parameters = init_parameters(fc_net)
# 初始化权值
AL, cache = forward_pass(train_set_x, parameters, active_func = "tanh")
# 最后一层激活值AL = train_set_x 输入数据、parameters W、b 保存位置、激活函数是 tanh
现在我们来实现激活函数。
def sigmoid(Z):
return 1 / (1 + np.exp(-Z))
def tanh(Z):
return np.tanh(Z)
def ReLU(Z):
return np.maximum(0,Z)
再来实现前向传播 forward_pass
。
def forward_pass(A0, parameters, active_func = "ReLU"):
Layer_num = len(parameters) // 2 # 因为 parameters 是由 W、b 组成,所以除 2
A = A0
cache = {} # 缓存 A 的字典
cache["A0"] = A0 # 先缓存A0
for L in range(1,Layer_num):
A_prev = A
Z = np.dot(parameters["W"+str(L)],A_prev) + parameters["b"+str(L)] # Z = WX + b
cache["Z"+str(L)] = Z # 缓存Z1 Z2 Z3 Z4
if active_func == "sigmoid":
A = sigmoid(Z) # sigmoid函数,适合用于深度网络
elif active_func == "tanh":
A = tanh(Z) # tanh函数激活
else:
A = ReLU(Z) # 1~3层用 ReLU 函数激活
cache["A"+str(L)] = A # 继续缓存 A1 A2 A3 A4
# 最末层采用sigmoid函数激活
ZL = np.dot(parameters["W" + str(Layer_num)], A) + parameters["b" + str(Layer_num)] # 1,2 2,209
cache["Z" + str(Layer_num)] = ZL # 缓存最末层的Z4
AL = sigmoid(ZL) # sigmoid函数激活
cache["A"+str(Layer_num)]=AL # 继续缓存最末层的A4
return AL, cache
对比《深度学习食用指南》识别猫的项目,这里多了一个要缓存 A,这是为啥?
其实是一种算法策略:空间换时间,因为前向传播过程如下所示:
一般的前向传播过程,我们是一个一个计算的,处理完一个样本再处理下一个样本,等所有样本都处理完了,才能反向传播,求参数的梯度。
能不能同时计算呢?
是输入样本,是不需要计算就可以得到的,我们把上面的
也堆叠起来,后面的输出值
就把多个样本中用 for 循环实现的多步串行计算,改成了用矩阵实现的一步完成的并行计算。
得到本轮迭代的损失值
if __name__ == '__main__':
train_set_x, train_set_y, test_set_x, test_set_y = load_dataset()
# 加载数据
fc_net = [12288, 4, 3, 2, 1]
# 输入层->第1层->第2层->第3层->第4层->a
# 输出值a,a>0.5 ? 1:0
parameters = init_parameters(fc_net)
# 初始化权值
AL,cache= forward_pass(train_set_x, parameters)
# 最后一层激活值AL = train_set_x 输入数据、parameters W、b 保存位置
cost = compute_cost(AL, train_set_y)
使用损失函数计算,预测值与标签值(真实值)的差距。
def compute_cost(AL, Y):
m = Y.shape[1] # Y =(1,209)
cost = (1/m)*np.sum((1/2)*(AL-Y)*(AL-Y)) # 代价函数
return cost
求最末层误差,反向传播计算各层梯度
if __name__ == '__main__':
train_set_x, train_set_y, test_set_x, test_set_y = load_dataset()
# 加载数据
fc_net = [12288, 4, 3, 2, 1]
# 输入层->第1层->第2层->第3层->第4层->a
# 输出值a,a>0.5 ? 1:0
parameters = init_parameters(fc_net)
# 初始化权值
AL,cache= forward_pass(train_set_x, parameters)
# 最后一层激活值AL = train_set_x 输入数据、parameters W、b 保存位置
cost = compute_cost(AL, train_set_y)
gradient = backward_pass(AL, parameters, cache, train_set_y)
# 反向传播计算梯度
我们来实现反向传播 backward_pass
。
def backward_pass(AL, parameters, cache, Y):
m = Y.shape[1] # 样本总数
gradient = {} # 保存各层参数梯度的字典
Layer_num = len(parameters) // 2
dZL = (AL-Y)*(AL*(1-AL)) # 获取最末层误差信号 dZL.shape = (1,209)
gradient["dW"+str(Layer_num)] = (1/m)*np.dot(dZL,cache["A"+str(Layer_num-1)].T)
gradient["db"+str(Layer_num)] = (1/m)*np.sum(dZL,axis=1,keepdims = True)
for L in reversed(range(1,Layer_num)): # 遍历[3,2,1],其中reversed函数[1,2,3]颠倒为[3,2,1]
dZL = np.dot(parameters["W"+str(L+1)].T,dZL)*(AL*(1-AL))
gradient["dW"+str(L)] = (1/m)*np.dot(dZL,cache["A"+str(L-1)].T)
gradient["db"+str(L)] = (1/m)*np.sum(dZL,axis=1,keepdims = True)
return gradient
根据各层的w、b梯度,使用梯度下降更新一次参数
if __name__ == '__main__':
train_set_x, train_set_y, test_set_x, test_set_y = load_dataset()
# 加载数据
fc_net = [12288, 4, 3, 2, 1]
# 输入层->第1层->第2层->第3层->第4层->a
# 输出值a,a>0.5 ? 1:0
parameters = init_parameters(fc_net)
# 初始化权值
AL,cache= forward_pass(train_set_x, parameters)
# 最后一层激活值AL = train_set_x 输入数据、parameters W、b 保存位置
cost = compute_cost(AL, train_set_y)
gradient = backward_pass(AL, parameters, cache, train_set_y)
# 反向传播计算梯度
iterations = 500
LearnRate = 0.01
costs = []
# 保存我们每次迭代计算得到的代价值
parameters = update_parameters(gradient, parameters, LearnRate)
# 根据梯度更新一次参数
得到梯度后,实现梯度下降。
def update_parameters(gradient,parameters,LearnRate):
# w : =w-r*dw、b := b-r*db
Layer_num = len(parameters) // 2
for L in range(1,Layer_num+1):
parameters["W"+str(L)] = parameters["W"+str(L)] - LearnRate*gradient["dW"+str(L)]
parameters["b"+str(L)] = parameters["b"+str(L)] - LearnRate*gradient["db"+str(L)]
return parameters
重复以上步骤,直到损失值低于设定阈值,模型收敛
if __name__ == '__main__':
train_set_x, train_set_y, test_set_x, test_set_y = load_dataset()
# 加载数据
fc_net = [12288, 4, 3, 2, 1]
# 输入层->第1层->第2层->第3层->第4层->a
# 输出值a,a>0.5 ? 1:0
parameters = init_parameters(fc_net)
# 初始化权值
iterations = 500
LearnRate = 0.01
costs = [] # 保存我们每次迭代计算得到的代价值
for iteration in range(0, iterations):
AL, cache = forward_pass(train_set_x, parameters)
# 计算代价值
cost = compute_cost(AL, train_set_y)
if iteration%100 == 0: # 每100个迭代周期打印一次代价值
print("iteration = ", iteration,"; cost = ", cost)
costs.append(cost)
# 反向传播计算梯度
gradient = backward_pass(AL, parameters, cache, train_set_y)
# 根据梯度更新一次参数
parameters = update_parameters(gradient, parameters, LearnRate)
封装为一个 model
函数,参数定义、数据加载并没有列入。
def model(fc_net, train_set_x, train_set_y, iterations=2000, LearnRate=0.01):
# 初始化参数
parameters = init_parameters(fc_net)
costs = [] # 保存我们每次迭代计算得到的代价值
for iteration in range(0, iterations):
AL,cache = forward_pass(train_set_x, parameters)
# 计算代价值
cost = compute_cost(AL,train_set_y)
if iteration%500 == 0: # 每500个迭代周期打印一次代价值
print("iteration = ",iteration,"; cost = ",cost)
costs.append(cost)
# 反向传播计算梯度
gradient = backward_pass(AL, parameters, cache, train_set_y)
# 根据梯度更新一次参数
parameters = update_parameters(gradient, parameters, LearnRate)
return parameters
神经网络的探索
梯度检验,检测反向传播是否有问题
代价值正常的走势:
但迭代到一定程度时,后面已经停滞了:
会不会是反向传播实现的有问题呢?那我们可以使用梯度检验来做检测。
梯度的求得有俩种方法:
- 解析法:求得梯度解析表达式,通过表达式得到梯度(确切解,当前反向传播便是如此)
- 数值逼近:近似解
对比俩种方法的值,只要近似,就说明我们的反向传播没有问题。
# 模型中添加梯度检验
def model(fc_net, train_set_x, train_set_y, check=False, iterations=2000, LearnRate=0.01, active_func="ReLU"):
# 初始化参数
parameters = init_parameters(fc_net)
costs = [] # 保存我们每次迭代计算得到的代价值
for iteration in range(0, iterations):
AL, cache = forward_pass(train_set_x, parameters)
# 计算代价值
cost = compute_cost(AL, train_set_y)
if iteration%500 == 0: # 每500个迭代周期打印一次代价值
print("iteration = ",iteration,"; cost = ",cost)
costs.append(cost)
# 反向传播计算梯度
gradient = backward_pass(AL, parameters, cache, train_set_y)
if check and iteration==2000:
gradient_check(train_set_x, train_set_y, gradient, parameters)
# 根据梯度更新一次参数
parameters = update_parameters(gradient, parameters, LearnRate)
return parameters
if __name__ == '__main__':
# 加载数据
train_set_x, train_set_y, test_set_x, test_set_y = load_dataset()
# 定义全连接神经网络各层神经元个数,并初始化参数w和b
fc_net = [12288,10,3,2,1] # 四层网络(梯度消失)
# 开始训练
parameters = model(fc_net, train_set_x, train_set_y, iterations=8000, LearnRate=0.1, active_func="sigmoid")
gradient_check
函数实现:
def gradient_check(A0, Y, gradient, parameters, epsilon=1e-4):
grad_vec = grad_dict_to_vector(gradient) # 梯度字典转列向量
param_vec = param_dict_to_vector(parameters)# 参数字典转列向量(49182,1)
param_num = param_vec.shape[0] # 49182
grad_vec_approach = np.zeros(grad_vec.shape)
for i in range(param_num):
if i%1000==0:
print("grad checking i=",i)
param_vec_plus = np.copy(param_vec)
param_vec_plus[i][0] = param_vec_plus[i][0] + epsilon
AL,_ = forward_pass(A0,vector_to_param_dict(param_vec_plus,parameters))
J_plus_epsilon = compute_cost(AL,Y)
param_vec_minus = np.copy(param_vec)
param_vec_minus[i][0] = param_vec_minus[i][0] - epsilon
AL,_ = forward_pass(A0,vector_to_param_dict(param_vec_minus,parameters))
J_minus_epsilon = compute_cost(AL,Y)
grad_vec_approach[i][0]= (J_plus_epsilon-J_minus_epsilon)/(2*epsilon)
# 在机器学习中,表征两个向量之间差异性的方法:L2范数(欧氏距离)、余弦距离
# L2范数:主要用于表征两个向量之间数值的差异(适合现在的情况)
# 余弦距离:主要用于表征两个向量之间方向的差异
diff = np.sqrt(np.sum((grad_vec-grad_vec_approach)**2))/(np.sqrt(np.sum((grad_vec)**2))+np.sqrt(np.sum((grad_vec_approach)**2)))
if diff > 1e-4:
print("Maybe a mistake in your bakeward pass!!! diff=",diff)
else:
print("No problem in your bakeward pass!!! diff=",diff)
gradient_check
函数还需要几个辅助函数:
-
grad_dict_to_vector(parameters)
:梯度字典转向量 -
grad_dict_to_vector(gradient)
:梯度字典转列矩阵 -
vector_to_param_dict
:列矩阵转参数字典 -
param_dict_to_vector
:参数字典转列矩阵
def grad_dict_to_vector(gradient): # 梯度字典转列矩阵
Layer_num = len(gradient) // 2
count = 0
for L in range(1,Layer_num+1):
dW_vector = np.reshape(gradient["dW"+str(L)], (-1,1)) # 将该层dW矩阵展平为一个列矩阵 (4,12288)->(49152,1)
db_vector = np.reshape(gradient["db"+str(L)], (-1,1)) # 将该层db矩阵展平为一个列矩阵 (4,1)->(4,1)
vec_L = np.concatenate((dW_vector, db_vector), axis=0) # 先将该层W个b串联叠加
if count == 0:
vec_output = vec_L # 叠加到输出列矩阵
else:
vec_output = np.concatenate((vec_output, vec_L), axis=0) # 逐层串联叠加
count = count + 1
return vec_output # 返回列矩阵
# 参数字典转列矩阵,将我们所有的参数字典转换为满足我们特定所需形状的单个向量
def param_dict_to_vector(parameters):
Layer_num = len(parameters) // 2
count = 0
for L in range(1,Layer_num+1):
W_vector = np.reshape(parameters["W"+str(L)], (-1,1)) # 将该层W参数矩阵展平为一个列矩阵
b_vector = np.reshape(parameters["b"+str(L)], (-1,1)) # 将该层b参数矩阵展平为一个列矩阵
vec_L = np.concatenate((W_vector, b_vector), axis=0) # 串联叠加
if count == 0:
vec_output = vec_L
else:
vec_output = np.concatenate((vec_output, vec_L), axis=0) # 串联叠加
count = count + 1
return vec_output
# 参数字典转列矩阵,将所有的参数字典转换为满足我们特定所需形状的单个向量
def param_dict_to_vector(parameters):
Layer_num = len(parameters) // 2
count = 0
for L in range(1,Layer_num+1):
W_vector = np.reshape(parameters["W"+str(L)], (-1,1)) # 将该层W参数矩阵展平为一个列矩阵
b_vector = np.reshape(parameters["b"+str(L)], (-1,1)) # 将该层b参数矩阵展平为一个列矩阵
vec_L = np.concatenate((W_vector, b_vector), axis=0) # 串联叠加
if count == 0:
vec_output = vec_L
else:
vec_output = np.concatenate((vec_output, vec_L), axis=0) # 串联叠加
count = count + 1
return vec_output
# 列矩阵转参数字典,第一个输入为列矩阵,第二个输入为保存 W 和 b 的参数字典
def vector_to_param_dict(vec, param_src):
Layer_num = len(param_src) // 2
param_epsilon = param_src
idx_start = 0
idx_end = 0
for L in range(1,Layer_num+1):
row = param_src["W"+str(L)].shape[0]
col = param_src["W"+str(L)].shape[1]
idx_end = idx_start + row*col # 该W参数矩阵元素个数
param_epsilon["W"+str(L)] = vec[idx_start:idx_end].reshape((row,col))
idx_start = idx_end
row = param_src["b"+str(L)].shape[0]
col = param_src["b"+str(L)].shape[1]
idx_end = idx_start+row*col # 该b参数矩阵元素个数
param_epsilon["b"+str(L)] = vec[idx_start:idx_end].reshape((row,col))
idx_start = idx_end
return param_epsilon # 返回添加了epsilon的参数字典
调用形式:
if __name__ == '__main__':
# 加载数据
train_set_x,train_set_y, test_set_x, test_set_y = load_dataset()
# 对输入像素值做归一化(0~255)->(0~1)
train_set_x = train_set_x/255.
test_set_x = test_set_x/255.
# 定义全连接神经网络各层神经元个数,并初始化参数w和b
fc_net = [12288,4,3,2,1]
parameters = model(fc_net, train_set_x, train_set_y, iterations=8000, LearnRate=0.01)
而后,我们看看预测结果。
if __name__ == '__main__':
# 加载数据
train_set_x, train_set_y, test_set_x, test_set_y = load_dataset()
# 定义全连接神经网络各层神经元个数,并初始化参数w和b
fc_net = [12288,10,3,2,1] # 四层网络(梯度消失)
# 开始训练
parameters = model(fc_net, train_set_x, train_set_y, iterations=8000, LearnRate=0.1, active_func="sigmoid")
# 开始预测
Predict(test_set_x, test_set_y, parameters)
def Predict(A0, Y, parameters):
AL, _ = forward_pass(A0, parameters)
m = AL.shape[1]
p = np.zeros(AL.shape)
for i in range(0,AL.shape[1]):
if AL[0,i] > 0.5:
p[0,i] = 1
else:
p[0,i] = 0
accuracy = (1/m)* np.sum(p==Y)
print("accuracy =", accuracy)
探索各种权重初始化
def init_parameters(fc_net):
#1.定义一个字典,存放参数矩阵W1,b1,W2,b2,W3,b3,W4,b4
parameters = {}
Layer_num = len(fc_net) #Layer_num=5
for L in range(1,Layer_num):
#parameters["W"+str(L)] = np.random.randn(fc_net[L],fc_net[L-1])*0.01 # 原来是 0.01
#parameters["W"+str(L)] = np.random.randn(fc_net[L],fc_net[L-1])*np.sqrt(1/fc_net[L-1]) # Xavier初始化,针对tanh函数
parameters["W"+str(L)] = np.random.randn(fc_net[L],fc_net[L-1])*np.sqrt(2/fc_net[L-1]) # He初始化(MSRA),针对ReLU函数
parameters["b"+str(L)] = np.zeros((fc_net[L],1))
for L in range(1,Layer_num):
print("W"+str(L)+"=",parameters["W"+str(L)].shape)
print("b"+str(L)+"=",parameters["b"+str(L)].shape)
return parameters
探索各种优化算法
批量归一化