吴恩达的DeepLearning.ai Lesson2主要是在Lesson1的基础上讲神经网络的参数调优,如何使NN变得更高效(正则化和初始参数),同时还介绍了如何debug神经网络。学完这些week1越来越感觉神经网络以及机器学习已经变成了一种玄学。接下来我总结一下这几周学到的玄学。本次实验(DeepLearning.ai Quiz)的完整代码(数据+代码)请点击这里

初始化(initialization)

对于一个神经网络,我们可以将其公式写为z=w1x1+w2x2++wnxnz = w_{1}x_{1} + w_{2}x_{2} + \cdots + w_{n}x_{n},如果是一个特别深的神经网络就意味着nn特别大,如果每一个wiw_{i}都比1大或都比1小就会使得最终的结果呈指数上升或呈指数下降,这时候就有可能造成梯度爆炸或梯度消失问题。但是目前仍然没有一个方法来彻底解决这个问题,只能通过调整初始化参数来缓解此类问题。

在编程作业中,Andrew Ng列举了三种参数(parameters)的初始化方式,第一种是统一把WWbb赋值为0,第二种是使用np.random.randn(shape_0, shape_1) * n赋值WWbb赋值为0,最后一种是根据He等人的一篇论文来确定的初始化的方式。

数据如下图所示,实验最后的要求是有效的将红色(值为0)和蓝色(值为1)分辨出来。

zeros

WW赋值为0的方式是错误的,在遍历之前每一层的都WW都是0,这样反向传播后的求导就会导致每一层的神经元学习到的东西是一样的,因为参数学习的公式为W=WαdWW = W - \alpha \cdot \mathrm{d}W,这一层的WW都会一起变化,所以Andrew Ng说用这种方式初始化参数WW会导致无法打破平衡(failing to break symmetry),最终会导致这一层就像只有一个神经元一样(n[l]=1n^{[l]} = 1),从整个神经网络来看就变成了一个逻辑回归,所以识别准确率将会大打折扣。

零初始化的核心代码:

"""
layers_dims: 每层神经元的数量的数组,如一个3层神经网络其layers_dims为[x, n1, n2, n3]
parameters: 参数字典, 例如parameters['W1']代表第一层的W参数
"""
for l in range(1, L):
   parameters['W' + str(l)] = np.zeros([layers_dims[l], layers_dims[l - 1]])
   parameters['b' + str(l)] = np.zeros([layers_dims[l], 1])

实验结果:

  • 训练集准确率50%
  • 测试集准确率50%
  • 训练集和测试集预测结果:
(训练集预测结果)predictions_train = 
[[0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
  0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
  0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
  0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
  0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
  0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
  0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
  0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
  0 0 0 0 0 0 0 0 0 0 0 0]]
(测试集预测结果)predictions_test = 
[[0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
  0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
  0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0]]
  • cost变化情况以及预测情况:

从实验结果上我们能看出来,这个网络对所有样本的预测结果都是0;这个网络的cost同样也没有实现梯度下降。所以用0来初始化参数WW是不可以的,但是可以用0来初始化bb

random

使用随机方式给WW赋值可以让每一个神经元学习不同的特征,通过接下来的实验结果我们很容易可以发现效果要比使用zeros方式有很大的提升。但是面临的问题是:

  • 初始化参数过大会导致一开始的cost过于大。因为一般在网络的最后使用的是sigmoid,很容易让值为0,在loss中使用的log函数,当x=0x = 0时就会导致无限大(inf);
  • WW过高或过低都会影响梯度下降的效果。

下面给出的是当初始化参数过大的情景,核心代码为:

for l in range(1, L):
    parameters['W' + str(l)] = np.random.randn(layers_dims[l], layers_dims[l-1]) * 10
    parameters['b' + str(l)] = np.zeros([layers_dims[l], 1])

实验结果:

  • 训练集准确率83%
  • 测试集准确率86%
  • cost变化情况以及预测情况:
# 每1000次输出的cost值
Cost after iteration 0: inf
Cost after iteration 1000: 0.6250982793959966
Cost after iteration 2000: 0.5981216596703697
Cost after iteration 3000: 0.5638417572298645
Cost after iteration 4000: 0.5501703049199763
Cost after iteration 5000: 0.5444632909664456
Cost after iteration 6000: 0.5374513807000807
Cost after iteration 7000: 0.4764042074074983
Cost after iteration 8000: 0.39781492295092263
Cost after iteration 9000: 0.3934764028765484
Cost after iteration 10000: 0.3920295461882659
Cost after iteration 11000: 0.38924598135108
Cost after iteration 12000: 0.3861547485712325
Cost after iteration 13000: 0.384984728909703
Cost after iteration 14000: 0.3827828308349524

he

这是根据He et al., 2015.介绍的一组初始化向量的方式,同时列举了一些常见的激活函数对应的初始化参数的方式:

  • ReLU: np.random.randn(shape) * 2n[l1]\sqrt{\frac{2}{n^{[l-1]}}}
  • tanh: np.random.randn(shape) * 1n[l1]\sqrt{\frac{1}{n^{[l-1]}}}np.random.randn(shape) * 2n[l1]+n[l]\sqrt{\frac{2}{n^{[l-1]} + n^{[l]}}}

核心代码(以ReLU为例):

for l in range(1, L):
    parameters['W' + str(l)] = np.random.randn(layers_dims[l], layers_dims[l-1]) * np.sqrt(2. / layers_dims[l-1])
    parameters['b' + str(l)] = np.zeros([layers_dims[l], 1])

实验结果:

  • 训练集准确率99%
  • 测试集准确率96%
  • cost变化情况以及预测情况:

正则化(regularization)

正则化在之前的文章中也提到过,主要通过减少权重(权重惩罚项)来减少过拟合的情况(over-fitting),前两篇文章传送门:神经网络中使用正则化减少方差Dropout正则化,之前只是理论的学习,通过编程作业之后我又对这两种正则化工具有了新的认识。

本次实验使用的数据集的2D图像为

不使用正则化

不使用正则化其实就是普通的神经网络,之前也有实现过,详情可以看MrN1u/deep-learning.ai/l1_w4DeepLearning.ai Lesson1 Summary。这里就不细说了,所以直接上训练结果。

在这个图上我们可以很容易的看到红色区域有过拟合的问题,验证集结果也能很容易的看到问题所在:

  • 训练集准确率94.79%
  • 测试集准确率91.5%

L2正则化(L2-Regularization)

之前我的文章中有重点描述过L2正则化的问题,通过权重惩罚项来达到解决过拟合的问题,所以在这里只是简单的总结一下。

正向传播

L2正则化瞄准的是Cost函数,(1)式是原先的cost函数,(2)是引入了正则项的式子。

在python中求kjWk,j[l]2\sum\limits_k\sum\limits_j W_{k,j}^{[l]2}使用

np.sum(np.square(Wl))

计算Cost的核心算法是(以一个3层网络为例)

# lambd是超参数(0-1之间)
# W*是每层的参数
L2_regularization_cost = (lambd / (2. * m)) * (np.sum(np.square(W1)) + np.sum(np.square(W2)) + np.sum(np.square(W3)))

反向传播

正向传播一切还是OK的,但是在反向传播中就产生了问题,因为Cost函数变了,那么计算导数的公式也会发生改变,但是:

  • 往哪加?
  • 加什么?

就成了困扰我的问题了,我们求导求的是偏导,dW\mathrm{d}W是求WW的偏导,那么自然就需要牵扯到新加入的L2正则式,因为正则式里有WW,但是对于db\mathrm{d}b来说就不一样了,正则式里没有bb,那么求db\mathrm{d}b自然就不需要加了。

解决了往哪加的问题我们再来解决加什么。后面的正则式中我们可以很明显的看出来1mλ2(kjWk,j[1]2+kjWk,j[2]2++kjWk,j[l]2)\frac{1}{m} \frac{\lambda}{2}(\sum\limits_k\sum\limits_j W_{k,j}^{[1]2} + \sum\limits_k\sum\limits_j W_{k,j}^{[2]2} + \cdots + \sum\limits_k\sum\limits_j W_{k,j}^{[l]2}),所以我们应该加的是每一层WW的导数,也就是ddW(12λmW2)=λmW\frac{d}{dW} ( \frac{1}{2}\frac{\lambda}{m} W^2) = \frac{\lambda}{m} * W,所以核心代码如下:

# 第3层
dZ3 = A3 - Y
dW3 = 1./m * np.dot(dZ3, A2.T) + (lambd / m) * W3	# (lambd / m) * W3
db3 = 1./m * np.sum(dZ3, axis=1, keepdims = True)

# 第2层
dA2 = np.dot(W3.T, dZ3)
dZ2 = np.multiply(dA2, np.int64(A2 > 0))
dW2 = 1./m * np.dot(dZ2, A1.T) + (lambd / m) * W2	# (lambd / m) * W2
db2 = 1./m * np.sum(dZ2, axis=1, keepdims = True)

# 第1层
dA1 = np.dot(W2.T, dZ2)
dZ1 = np.multiply(dA1, np.int64(A1 > 0))
dW1 = 1./m * np.dot(dZ1, X.T) + (lambd / m) * W1	# (lambd / m) * W1
db1 = 1./m * np.sum(dZ1, axis=1, keepdims = True)

所以最后的结果为:

这个图可以看到过拟合情况已经明显改善,数据情况为:

  • 训练集准确率93.83%
  • 测试集准确率93%

Dropout Regularization

在编程过程中需要注意的几点:

  • 每次迭代都重新生成随机数(每次迭代都随机删除某些节点),并不是训练一次中不变了;
  • 一次迭代中正向传播和反向传播需要随一样随机删除节点,所以正向传播结束后需要缓存住删除节点的信息;
  • 记得每次正向/反向传播结束后A/=keep_probA /= keep\_prob来补偿损失;
  • 测试集的时候不要再使用dropout了。

核心代码

# 正向传播
# (LINEAR -> RELU) -> (LINEAR -> RELU) -> (LINEAR -> SIGMOID)
Z1 = np.dot(W1, X) + b1
A1 = relu(Z1)

D1 = np.random.rand(A1.shape[0], A1.shape[1])
D1 = (D1 < keep_prob)	# 小于keep_prob为True, 大于为False
A1 = np.multiply(A1, D1)	# 随机删除节点
A1 /= keep_prob		# 除以keep_prob补偿结果

Z2 = np.dot(W2, A1) + b2
A2 = relu(Z2)
D2 = np.random.rand(A2.shape[0], A2.shape[1])
D2 = (D2 < keep_prob)
A2 = np.multiply(A2, D2)
A2 /= keep_prob

Z3 = np.dot(W3, A2) + b3
A3 = sigmoid(Z3)

# 反向传播
dZ3 = A3 - Y
dW3 = 1. / m * np.dot(dZ3, A2.T)
db3 = 1. / m * np.sum(dZ3, axis=1, keepdims=True)
dA2 = np.dot(W3.T, dZ3)
dA2 = np.multiply(dA2, D2)	# 删除与迭代的正向传播相同的随机节点
dA2 /= keep_prob
dZ2 = np.multiply(dA2, np.int64(A2 > 0))
dW2 = 1. / m * np.dot(dZ2, A1.T)
db2 = 1. / m * np.sum(dZ2, axis=1, keepdims=True)

dA1 = np.dot(W2.T, dZ2)
dA1 = np.multiply(dA1, D1)
dA1 /= keep_prob
dZ1 = np.multiply(dA1, np.int64(A1 > 0))
dW1 = 1. / m * np.dot(dZ1, X.T)
db1 = 1. / m * np.sum(dZ1, axis=1, keepdims=True)

所以最后的结果为:

和L2正则化一样情况有了明显的改善,数据情况为:

  • 训练集准确率92.89%
  • 测试集准确率95%

梯度检测(Gradient Checking)

梯度检测主要用来检测我们的神经网络是否存在bug,梯度检测就是默认正向传播是正确的,出错的位置应该在反向传播中。整个工作的核心为$ \frac{\partial J}{\partial \theta} = \lim_{\varepsilon \to 0} \frac{J(\theta + \varepsilon) - J(\theta - \varepsilon)}{2 \varepsilon},只不过这时候的\theta$是神经网络全部参数的摊成的向量。具体步骤为:

  1. 把所有的参数转换成一个巨大的向量(parameters_values),如下图所示,会把每一个参数摊开,比如W1W_{1}是一个5 x 5的矩阵,摊开就是25个参数,需要注意的是参数bb虽然在python中可以通过broadcast特性自动展开,但是在这一步中需要严格按照本来的shape去摊开;
  1. 遍历i in parameters_values.shape[0]:
    1. 获取θi++ε\theta^{+}_i + \varepsilon,**这一步是对单个参数增加ε\varepsilon,**以i = 0为例θ1++ε\theta^{+}_1 + \varepsilon(W111+ε,W112,,b31)(W^{11}_1 + \varepsilon, W^{12}_1, \cdots, b^{1}_3)
    2. 计算J(θi++ε)J(\theta^{+}_i + \varepsilon)J(W)J(W)就是cost函数;
    3. 同理计算J(θi+ε)J(\theta^{+}_i - \varepsilon)
    4. 最后计算gradapproxi=J(θi++ε)J(θi+ε)2εgradapprox_i = \frac{J(\theta^{+}_i + \varepsilon) - J(\theta^{+}_i - \varepsilon)}{2\varepsilon}
  2. 最后计算difference=gradgradapprox2grad2+gradapprox2difference = \frac {\| grad - gradapprox \|_2}{\| grad \|_2 + \| gradapprox \|_2 },其中gradgrad是正常使用正向传播计算出的。

一般来说当ε=e7\varepsilon = e^{-7}时,如果difference>e7difference \gt e^{-7}则说明可能神经网络存在问题,如果反之则说明没有问题。

# 核心代码
for i in range(num_parameters):
    thetaplus = np.copy(parameters_values)  # Step 1
    thetaplus[i][0] = thetaplus[i][0] + epsilon  # Step 2: 这一步记住是对单个参数增加epsilon
    J_plus[i], _ = forward_propagation_n(X, Y, vector_to_dictionary(thetaplus))  # Step 3

    thetaminus = np.copy(parameters_values)  # Step 1
    thetaminus[i][0] = thetaminus[i][0] - epsilon  # Step 2
    J_minus[i], _ = forward_propagation_n(X, Y, vector_to_dictionary(thetaminus))  # Step 3
    gradapprox[i] = (J_plus[i] - J_minus[i]) / (2 * epsilon)

numerator = np.linalg.norm(grad - gradapprox)  # Step 1'
denominator = np.linalg.norm(grad) + np.linalg.norm(gradapprox)  # Step 2'
difference = numerator / denominator  # Step 3'