赞
踩
本文是基于吴恩达老师的深度学习课程系列中第一门课所写,以最终实现手撸一份神经网络为作业而成。本文只为写下在实现网络过程中,自己感到过困惑的地方的总结,未覆盖实现全连接层神经网络的所有知识细节,如需更多深入了解,可以直达课程笔记。
由于吴恩达老师的课程作业中大部分框架都已经搭好了,只需自己实现部分内容,导致忽略框架部分的实现,因此重新手写了所有代码(仍保留了原函数接口)。由于难点在于训练神经网络,因此只记录了与训练神经网络相关的步骤,使用神经网络(计算误差、预测)等部分未记录在此。
import numpy as np
def relu(Z):
return np.maximum(Z, 0)
使用 numpy.maximum
函数,将两个数组传入其中,函数对每个索引上位置进行比较并取最大值。而 numpy.max
是另外一个完全不同的函数,传入一个数组,它返回的是这个数组(或者指定某一维)的最大值。
def sigmoid(Z):
return 1 / (1+np.exp(-Z))
使用 numpy.exp
获取
e
x
e^x
ex 函数的值。
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) =
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
进行向量化计算。更多复杂的函数和向量化计算还需要更多时间的积累。然后介绍了训练神经网络的五个步骤,每个步骤中都有一些需要特别注意和理解的地方。理解了每个细节之后,应该就能理解神经网络的整个训练过程了。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。