7. 损失函数
损失函数可以监测训练进展,确保我们向着正确的方向移动。“一般来说,损失函数显示了我们与’理想’解决方案之间的距离。”损失函数包含很多种(例如 Pytorch 中提供了很多选项,可见其官方网站 https://pytorch.org/docs/stable/nn.html#loss-functions ),我们通常需要根据具体问题来选择。
我们计划将神经网络应用于二元分类问题,即模型最后的预测结果只有两种情况,对于每个类别我们预测得到的概率将分别为 𝑝 和 1−𝑝。在这个问题中我们将使用交叉熵损失函数 (cross entropy loss):
def get_cost_value(Y_hat, Y): # number of examples (=900) m = Y_hat.shape[1] # calculation of the cost according to the formula # - Y: each element is either 0 or 1 # - shape of Y & Y_hat: (1,900) # - shape of cost: (1,1) cost = -1 / m * (np.dot(Y, np.log(Y_hat).T) + np.dot(1 - Y, np.log(1 - Y_hat).T)) return np.squeeze(cost) # np.squeeze() - 从数组的形状中删除单维条目,即把shape中为1的维度去掉
为了取得更多关于学习过程的信息,我们还可以另外实现一个计算精确度的函数。
# an auxiliary function that converts probability into class def convert_prob_into_class(probs): probs_ = np.copy(probs) probs_[probs_ > 0.5] = 1 probs_[probs_ <= 0.5] = 0 return probs_ def get_accuracy_value(Y_hat, Y): Y_hat_ = convert_prob_into_class(Y_hat) # shape: (1,900) accuracy = (Y_hat_ == Y).all(axis=0).mean() # numpy.all(): 测试沿给定轴的所有数组元素是否都计算为True # (Y_hat_ == Y).all(axis=0).shape = (900,) # numpy.mean(): 求取均值 return accuracy
8. 实现神经网络的反向传播过程
和前向传播一样,我们将把网络的反向传播过程拆分成两个函数来实现:
函数 8-1 (single_layer_backward_propagation): 实现单个全连接层的反向传播过程
函数 8-2 (full_backward_propagation): 基于函数 8-1,实现整个神经网络的反向传播过程
8.1 函数 8-1 (single_layer_backward_propagation): 实现单个全连接层的反向传播过程
下图显示了单个神经元上的前向传播和反向传播过程。对一个神经网络层 (𝑙),我们将接收传输自 𝑙+1 层的 𝑑𝐴[𝑙],并根据前向传播时保存在 𝑚𝑒𝑚𝑜𝑟𝑦 的中间数据 𝑍_𝑐𝑢𝑟𝑟 和 𝐴_𝑝𝑟𝑒𝑣,计算 𝑙𝑜𝑠𝑠对权重 𝑊[𝑙]和偏置 𝑏[𝑙]的偏导数——𝑑𝑊[𝑙]和 𝑑𝑏[𝑙]。
下面的公式描述了单个全连接层上的反向传播过程。由于本次实验的重点在于实际实现,所以此处省略求导过程,感兴趣的同学请阅读课本相关章节。从公式上我们可以很明显地看到,为什么之前需要在前向传播时记住中间层的 𝐴_𝑝𝑟𝑒𝑣 和 𝑍_𝑐𝑢𝑟𝑟 矩阵的值。
def single_layer_backward_propagation(dA_curr, W_curr, b_curr, Z_curr, A_prev, activation="relu"): # number of examples m = A_prev.shape[1] # selection of activation function if activation is "relu": backward_activation_func = relu_backward elif activation is "sigmoid": backward_activation_func = sigmoid_backward else: raise Exception('Non-supported activation function') # step-1: {dA_curr, Z_curr} -> dZ_curr (activation function derivative) dZ_curr = backward_activation_func(dA_curr, Z_curr) # step-2: {dZ_curr, A_prev} -> dW_curr (derivative of the matrix W) # dZ_curr -> db_curr (derivative of the vector b) ############### Please finish this part ################ # [Hint]: get dW_curr and db_curr from dZ_curr and A_prev # dW_curr = np.dot(XXX,XXX) / m dW_curr = np.dot(dZ_curr, A_prev.T) / m ######################## end ########################### db_curr = np.sum(dZ_curr, axis=1, keepdims=True) / m # np.sum(): sum of array elements over a given axis # or, you can write it as: # db_curr = np.dot(dZ_curr, np.ones((dZ_curr.shape[1], 1))) / m # dZ_curr.shape[1] = 900 # step-3: {dZ_curr, W_curr} -> dA_prev (derivative of the matrix A_prev) ############### Please finish this part ################ # [Hint]: get dA_prev from dZ_curr and W_curr # dA_prev = np.dot(XXX,XXX) dA_prev = np.dot(W_curr.T, dZ_curr) ######################## end ########################### return dA_prev, dW_curr, db_curr
代码解释:根据公式可知,矩阵dW_curr的微分是(dZl*A[l-1]T)/m,不难发现dZl正是向量dZ_curr,而A[l-1]T则是向量A_prev的转置。向量dA_prev是 WlTdZl*,WlT是向量W_curr的转置,dZ*l是向量dZ_curr。
8.2 函数 8-2 (full_backward_propagation): 实现整个神经网络的反向传播过程
基于单个网络层的反向传播函数,我们从最后一层开始迭代计算所有参数上的导数(𝑑𝑊 和 𝑑𝑏),并最终返回包含所需梯度的字典。为了计算各层参数上的导数,函数需要输入前向传播时的网络输出 𝑌_ℎ𝑎𝑡 和标准结果 𝑌,并交由损失函数计算 𝑙𝑜𝑠𝑠。𝑙𝑜𝑠𝑠对于 𝑌_ℎ𝑎𝑡 的偏导数可计算如下:
此外,函数还需要输入如下信息:
前向传播时的中间数据 𝑚𝑒𝑚𝑜𝑟𝑦(含 𝐴_𝑝𝑟𝑒𝑣 和 𝑍_𝑐𝑢𝑟𝑟) —— 用以计算各层的 𝑑𝑊 和 𝑑𝑏;
神经网络的参数 𝑝𝑎𝑟𝑎𝑚𝑠_𝑣𝑎𝑙𝑢𝑒𝑠 —— 用以计算各层的 𝑑𝐴_𝑝𝑟𝑒𝑣。
函数的输出是各层的 𝑑𝑊 和 𝑑𝑏,保存在 𝑔𝑟𝑎𝑑𝑠_𝑣𝑎𝑙𝑢𝑒𝑠 中。
在编写函数8-2前,我们先来试验一下等下将怎样从最后一层开始,向前逐层处理网络:
# let's see how we will access each layer in the backward pass for layer_idx_prev, layer in reversed(list(enumerate(NN_ARCHITECTURE))): # we will use this code line in the function 8-2 print(layer_idx_prev) # from 4 to 0 print(layer) # from layer L to layer 1 (we number network layers from 1)
4 {'input_dim': 25, 'output_dim': 1, 'activation': 'sigmoid'} 3 {'input_dim': 50, 'output_dim': 25, 'activation': 'relu'} 2 {'input_dim': 50, 'output_dim': 50, 'activation': 'relu'} 1 {'input_dim': 25, 'output_dim': 50, 'activation': 'relu'} 0 {'input_dim': 2, 'output_dim': 25, 'activation': 'rel
def full_backward_propagation(Y_hat, Y, memory, params_values, nn_architecture): grads_values = {} # number of examples (Y.shape=(1,900)) m = Y.shape[1] # a hack ensuring the same shape of the prediction vector and labels vector Y = Y.reshape(Y_hat.shape) # initiation of gradient descent algorithm dA_prev = - (np.divide(Y, Y_hat) - np.divide(1 - Y, 1 - Y_hat)); for layer_idx_prev, layer in reversed(list(enumerate(nn_architecture))): # we number network layers from 1 layer_idx_curr = layer_idx_prev + 1 # extraction of the activation function for the current layer activ_function_curr = layer["activation"] dA_curr = dA_prev A_prev = memory["A" + str(layer_idx_prev)] Z_curr = memory["Z" + str(layer_idx_curr)] W_curr = params_values["W" + str(layer_idx_curr)] b_curr = params_values["b" + str(layer_idx_curr)] dA_prev, dW_curr, db_curr = single_layer_backward_propagation( dA_curr, W_curr, b_curr, Z_curr, A_prev, activ_function_curr) grads_values["dW" + str(layer_idx_curr)] = dW_curr grads_values["db" + str(layer_idx_curr)] = db_curr return grads_values
9. 更新参数值
反向传播是为了计算梯度,以根据梯度进行优化,更新网络的参数值。为了完成这一任务,我们将使用如下两组数据:
𝑝𝑎𝑟𝑎𝑚𝑠_𝑣𝑎𝑙𝑢𝑒𝑠,其中保存了当前参数值;
𝑔𝑟𝑎𝑑𝑠_𝑣𝑎𝑙𝑢𝑒𝑠,其中保存了用于更新参数值所需的梯度信息。
接下来我们只需在每个网络层上应用以下公式即可。其中,𝛼 指学习率 (learning rate)。
def update(params_values, grads_values, nn_architecture, learning_rate): # iteration over network layers for idx, layer in enumerate(nn_architecture): layer_idx = idx + 1 ############### Please finish this part ################ # [Hint]: update params_values # params_values["W" + str(layer_idx)] = XXX # params_values["b" + str(layer_idx)] = XXX params_values["W" + str(layer_idx)] -= learning_rate * grads_values["dW" + str(layer_idx)] params_values["b" + str(layer_idx)] -= learning_rate * grads_values["db" + str(layer_idx)] ######################## end ########################### return params_values
代码解释:每一层的权重矩阵 𝑊 和偏置向量 𝑏均以Wl与bl的形式保存在params_values中,而grads_values中保存了用于更新参数值所需的梯度信息。根据已经给出的公式可知,参数的更新是每次减去梯度乘以学习率,据此可以填写代码。
10. 整合:完成 train 函数
到目前为止,我们已经完成了最困难的部分,准备好了所需的函数,接下来只需以正确的顺序把它们整合到一起。为了更好地理解每一步操作的顺序,请同学们再次查看“0. 概览”中的训练流程图。
train 函数将返回经过训练优化后的参数值,并显示在训练期间模型在训练集上的准确率。后期在测试集上,只需使用训练好的模型参数并运行一次完整的前向传播过程即可。
def train(X, Y, nn_architecture, epochs, learning_rate, verbose=True, callback=None): # initiation of neural net parameters params_values = init_layers(nn_architecture, 2) # initiation of lists storing the history of metrics calculated during the learning process cost_history = [] accuracy_history = [] # performing calculations for subsequent iterations for i in range(epochs): # step forward Y_hat, cache = full_forward_propagation(X, params_values, nn_architecture) if i==1: print("X.shape: ", X.shape) # (2, 900) print("Y_hat.shape: ", Y_hat.shape) # (1, 900) print("Y.shape: ", Y.shape) # (1, 900) # calculating metrics and saving them in history cost = get_cost_value(Y_hat, Y) cost_history.append(cost) accuracy = get_accuracy_value(Y_hat, Y) accuracy_history.append(accuracy) # step backward - calculating gradient grads_values = full_backward_propagation(Y_hat, Y, cache, params_values, nn_architecture) # updating model state params_values = update(params_values, grads_values, nn_architecture, learning_rate) if(i % 50 == 0): if(verbose): print("Iteration: {:05} - cost: {:.5f} - accuracy: {:.5f}".format(i, cost, accuracy)) if(callback is not None): callback(i, params_values) return params_values