吴恩达深度学习课程作业记录 - 训练深度神经网络
本文是基于吴恩达老师的深度学习课程系列中第一门课所写,以最终实现手撸一份神经网络为作业而成。本文只为写下在实现网络过程中,自己感到过困惑的地方的总结,未覆盖实现全连接层神经网络的所有知识细节,如需更多深入了解,可以直达课程笔记。深度学习笔记-目录由于吴恩达老师的课程作业中大部分框架都已经搭好了,只需自己实现部分内容,导致忽略框架部分的实现,因此重新手写了所有代码(仍保留了原函数接口)。由于难点在于
零、前言
本文是基于吴恩达老师的深度学习课程系列中第一门课所写,以最终实现手撸一份神经网络为作业而成。本文只为写下在实现网络过程中,自己感到过困惑的地方的总结,未覆盖实现全连接层神经网络的所有知识细节,如需更多深入了解,可以直达课程笔记。
由于吴恩达老师的课程作业中大部分框架都已经搭好了,只需自己实现部分内容,导致忽略框架部分的实现,因此重新手写了所有代码(仍保留了原函数接口)。由于难点在于训练神经网络,因此只记录了与训练神经网络相关的步骤,使用神经网络(计算误差、预测)等部分未记录在此。
一、基础
使用 numpy 进行运算
- 向量化实现 Relu 函数
import numpy as np
def relu(Z):
return np.maximum(Z, 0)
使用 numpy.maximum
函数,将两个数组传入其中,函数对每个索引上位置进行比较并取最大值。而 numpy.max
是另外一个完全不同的函数,传入一个数组,它返回的是这个数组(或者指定某一维)的最大值。
- 向量化实现 Sigmoid 函数
def sigmoid(Z):
return 1 / (1+np.exp(-Z))
使用 numpy.exp
获取
e
x
e^x
ex 函数的值。
- 向量化实现 Relu 函数的导数
def relu_backward(Z):
result = np.array(Z.shape)
result[Z>0] = 1
result[Z<=0] = 0
return result
在 numpy
中,条件索引不仅可以用于选择数组中的元素, 还能用于给满足特定条件的元素赋值。虽然最终并没有直接用到这里实现的 relu_backward
方法,但会用到这里的 numpy
条件索引赋值语法。
二、训练神经网络
训练神经网络可以细分成五个步骤:
- 初始化神经网络参数
initialize_parameters
参数存储在一个字典中,键使用字符串 W0
, b0
… 来表示每层参数项,其中需要重点搞清楚的是层数和索引:
def initialize_parameters(layer_dims):
'''
:param layer_dims: 表示网络中每层的维度,从输入层 X 开始,到中间隐藏层,一直到最后输出层 Y
:return: 中间层的参数 W 和 b
'''
L = len(layer_dims)
parameters = {}
for i in range(1, L):
parameters["W" + str(i)] = np.random.randn(layer_dims[i], layer_dims[i-1]) / np.sqrt(layer_dims[i-1])
parameters["b" + str(i)] = np.zeros((layer_dims[i], 1))
return parameters
将神经网络的参数放到一个名为 parameters
的字典中,键值为字符串类型,例如第一层网络的参数 W1
,其设置方式为 parameters["W1"] = np.random.randn(...)
,通过使用这种字典可以省去变量的存储。
- 前向传播
L_model_forward
前向传播包含线性传播和激活,线性函数的结果被放入缓存中,同时激活函数的结果也会被放到缓存中,以便后续反向传播时直接使用。(为什么需要将结果放到缓存中,可以通过推导 sigmoid
, relu
的导数,可以看到导数值均和原来的输入相关,因此将前向传播的值缓存下来,以便求导时使用)。
s i g m o i d ′ ( x ) = x ( 1 − x ) sigmoid'(x) =x(1-x) sigmoid′(x)=x(1−x)
r e l u ′ ( x ) = { 1 if x > 0 , 0 if x ≤ 0. relu'(x) = \begin{cases} 1 & \text{if } x > 0, \\ 0 & \text{if } x \leq 0. \end{cases} relu′(x)={10if x>0,if x≤0.
L 层神经网络的前 L-1 层均为 relu
层,第 L 层为 sigmoid
层。
def linear_forward(A, W, b):
Z = np.dot(W, A) + b
cache = (A, W, b)
return Z, cache
def relu(Z):
return np.maximum(0, Z), Z
def sigmoid(Z):
return 1/(1+np.exp(-Z)), Z
def linear_forward_activation(A, W, b, activation):
Z, linear_cache = linear_forward(A, W, b)
if activation == "relu":
A, activation_cache = relu(Z)
elif activation == "sigmoid":
A, activation_cache = sigmoid(Z)
return A, (linear_cache, activation_cache)
def L_model_forward(X, parameters):
# 这里的 L 表示除了输入层之外的所有层数
L = len(parameters) // 2
caches = []
A = X
for i in range(1, L):
W = parameters["W" + str(i)]
b = parameters["b" + str(i)]
A, cache = linear_forward_activation(A, W, b, "relu")
caches.append(cache)
W = parameters["W" + str(L)]
b = parameters["b" + str(L)]
AL, cache = linear_forward_activation(A, W, b, "sigmoid")
caches.append(cache)
return AL, caches
代码中需要注意的两个细节:
-
使用
parameters
来获取神经网络层数,由于parameters
包含的是所有层的W
和b
参数,且每层中W
和b
均只有一个,因此通过将paramenters
的长度整除以 2,即可得到神经网络的层数 L(python 中整除是以//
双斜杠表示)。 -
range
函数的取值从 1 到 L-1,而非从 0 开始,因为W
和b
参数没有第 0 层。
- 计算损失函数
compute_cost
计算损失函数的目的在于监督函数的学习走向,这一步并没有真的向神经网络输出真正的反馈。
def compute_cost(AL, Y):
m = Y.shape[1]
cost = - np.sum((Y * np.log(AL) + (1-Y) * np.log(1-AL))) / m
cost = np.squeeze(cost)
return cost
- 反向传播
L_model_backward
最难的部分在于反向传播,主要困难在于理解导数是如何传播的。
def relu_backward(dA, cache):
# print("cache shape " + str(cache.shape))
# print("dA shape " + str(dA.shape))
Z = cache
result = np.array(dA, copy=True)
result[Z < 0] = 0
assert result.shape == dA.shape
return result
def sigmoid_backward(dA, cache):
Z = cache
Y = 1/(1+np.exp(-Z))
result = dA * Y * (1-Y)
assert result.shape == dA.shape
return result
def linear_activation_backward(dA, cache, activation):
linear_cache, activation_cache = cache
if activation == "relu":
dZ = relu_backward(dA, activation_cache)
dA_prev, dW, db = linear_backward(dZ, linear_cache)
elif activation == "sigmoid":
dZ = sigmoid_backward(dA, activation_cache)
dA_prev, dW, db = linear_backward(dZ, linear_cache)
# print("linear_activation_backward db shape " + str(db.shape))
return dA_prev, dW, db
def linear_backward(dZ, cache):
m = dZ.shape[1]
A_prev, W, b = cache
dW = np.dot(dZ, A_prev.T) / m
db = (np.sum(dZ, axis=1) / m).reshape(-1, 1)
dA_prev = np.dot(W.T, dZ)
return dA_prev, dW, db
def L_model_backward(AL, Y, caches):
L = len(caches)
dAL = -np.divide(Y, AL) + np.divide(1-Y,1-AL)
grads = {}
cache = caches[L-1]
dA_prev, dW, db = linear_activation_backward(dAL, cache, "sigmoid")
grads["dW" + str(L)] = dW
grads["db" + str(L)] = db
for l in reversed(range(1, L)):
cache = caches[l-1]
dA_prev, dW, db = linear_activation_backward(dA_prev, cache, "relu")
grads["dW" + str(l)] = dW
grads["db" + str(l)] = db
return grads
有一些需要注意的地方:
-
如何理解
dAL
这个变量,首先需要知道定义的损失函数: J = − y l o g y ^ − ( 1 − y ) l o g ( 1 − y ^ ) J =-ylog\widehat{y}-(1-y)log(1-\widehat{y}) J=−ylogy −(1−y)log(1−y ),其中自变量为 y ^ \widehat{y} y ,也即输出值AL
,因此在求导后,得到的是这个函数在当前 y ^ = d A L \widehat{y}=dAL y =dAL 时的导数,这个导数代表了使得 J J J 下降最快的方向。(直观一点的理解,比如我们有一条轨道,轨道是有弧度的,轨道上有一个小球,此时我们需要给这个小球一个力,力的方向可以是任意的,那么如何让这个小球前进效率最高呢?就是沿着当前小球所处轨道的切线方向施加力,上面导数就是这个切线方向,而学习效率就是我们往这个方向上前进的步长)。在找到方向后,我们需要将这个方向分解到各个参数上(例如公司在找到战略方向后,会将战略任务分发到每个下级,让每个人都往一个方向发力),因此这里的dAL
就是帮助我们找到了一个使得损失函数下降的大方向,我们需要将它分解成每个参数的小方向dW(L)
,db(L)
,dW(L-1)
,db(L-1)
…dW(1)
,db(1)
-
计算
dAL
,这是最后一层的输出AL
与实际Y
值的交叉熵(损失)的导数。这里没有将所有损失加起来,而是每个样本都有自身的导数。 -
在计算
dW(l)
, 和db(l)
时将所有样本的导数进行平均(这里的平均使用了向量化计算) -
relu_backward
函数中,正常情况下导数计算公式为 d A ∗ r e l u ′ ( x ) dA * relu'(x) dA∗relu′(x),由于 relu 的导数在x > 0
时为 1, 在x < 0
时为 0,因此这里直接使用了一个dA
数组的拷贝,并使用dA < 0
构建条件索引,并使用条件索引赋值dA[dA<0] = 0
将所有小于 0 的值变成 0,从而节省乘法的步骤。 -
linear_backward
函数中,计算 db 时将结果进行了reshape(-1, 1)
操作。原因是在np.sum(xxx, axis=1)
函数计算完之后得到的是一个一维数组,输出其 shape 可以发现是(xx,)
这种格式,而非我们预期中的(xx,1)
。前者在进行广播计算的时候会出现一些意想不到的情况,例如在后续update_parameters
中,将(xx,)
和一个(xx, 1)
类型的值进行加减运算时,会得到一个(xx,xx)
类型的数组。(这是因为(xx,)
类型的数组会在行上进行复制,而(xx,1)
的数组会在列上进行复制,从而使得两者的维度保持一致)
- 更新参数
update_parameters
def update_parameters(parameters, grads, learning_rate):
L = len(parameters) // 2
for l in range(1, L+1):
before_shape = parameters["b" + str(l)].shape
parameters["W" + str(l)] = parameters["W" + str(l)] - grads["dW" + str(l)] * learning_rate
parameters["b" + str(l)] = parameters["b" + str(l)] - grads["db" + str(l)] * learning_rate
after_shape = parameters["b" + str(l)].shape
assert before_shape == after_shape
这个部分的更新就是将每层的梯度 dW(l)
和 db(l)
更新到实际参数 W(l)
和 b(l)
上(这里就是由于踩了上面所说 b - learning_rate * db
时的坑,因此判断了下参数 b
在更新前后形状均不应该发生变化)。其他需要注意和上面一样,也就是 W
和 b
参数的索引是从 1 到 L,计算 L 的方式是通过 parameters
的长度整除以 2 得到。
三、总结
首先介绍了几个在实现神经网络中的函数时,如何使用 numpy
进行向量化计算。更多复杂的函数和向量化计算还需要更多时间的积累。然后介绍了训练神经网络的五个步骤,每个步骤中都有一些需要特别注意和理解的地方。理解了每个细节之后,应该就能理解神经网络的整个训练过程了。
更多推荐
所有评论(0)