吴恩达 改善深层神经网络:超参数调试、正则化以及优化

第一步 理解数据划分

对于一个需要解决的问题的样本数据,在建立模型的过程中,数据会被划分为以下几个部分:

训练集(train set):用训练集对算法或模型进行训练过程;
验证集(development set):利用验证集(又称为简单交叉验证集,hold-out cross validation set)进行交叉验证,选择出最好的模型;
测试集(test set):最后利用测试集对模型进行测试,获取模型运行的无偏估计(对学习方法进行评估)。

在小数据量的时代,如10000 的数据量大小,可以将数据集按照以下比例进行划分:

无验证集的情况:70% / 30%;
有验证集的情况:60% / 20% / 20%;

而在如今的大数据时代,对于一个问题,我们拥有的数据集的规模可能是百万级别的,所以验证集和测试集所占的比重会趋向于变得更小。
原因是因为验证集的目的是为了验证不同的算法哪种更有效,所以验证集只要足够大即可,不需要都要全量数据的20%,这样会过于让费。

100 万数据量:98% / 1% / 1%;
超百万数据量:99.5% / 0.25% / 0.25%(或者99.5% / 0.4% / 0.1%)

另外有一个很重要的点是,验证集要和训练集来自于同一分布,有一些我们自己上传的图片会发现模型预测不准,很大概率就是我们的训练数据的分布和你的测试集的图片不是一个分布。这里的分布指的是照片的源头,比如来自同一个摄像设备。同样的分辨率等。

第二步 理解偏差(bias)和方差(variance)

高偏差代表欠拟合。代表你的模型不能很好的适配训练数据。
高方差代表过拟合。代表你的模型不能很好的适配验证数据
下面分别给出了3个模型,蓝色的线是模型。


image.png

第一张图因为模型是线性的终究很难适配这种数据分布。第三张图,因为一个异常点而过分拟合了那个异常点,造成模型对验证数据不能游很好的适配效果。失去一般性。

当然有了2个维度。我们知道就会有4种情况。
第一种是高偏差和高方差。也就是说跑程序跑完,在训练集上准确率很低,在验证集上也很低,而且低的程度比训练集上差很多。
第二种就是在训练集上100%正确,但是再验证集上出现了20%的错。这个就是低偏差,高方差的问题。
第三种是在训练集上准确率低,但是验证集上差不多低。这就是单纯的高偏差问题。
我们最希望看到的是2个准确率都很高,那么就完美了。


image.png

人类可以达到的error称作为base error,或optimal error。如果上述表格中,设定的base error=15%,那么第二行的例子反而是low bias & low variance的。

那么应对方法如下:
高偏差的问题说明模型简单了不能很好的拟合训练数据。
所以我们可以(扩大网络规模,使得模型有更强的可表达性。 或者花费更长的时间训练。还有一种就是寻找更合适的网络架构,如我们之后会学的RNN和CNN)

高方差的问题:则可以用更大的数据集,正则化。当然寻找更合适的网络结构也有帮助。

image.png

在早期的ML,强调bias variance trade-off;但在现代deep learning,可以通过加大Neural Network或增加更多数据,在分别解决High bias和High variance的时候,并不会影响彼此。

第三步 学会正则化优化我们上次的项目的高方差问题

在上一篇博文中,我们自己动手写了一个神经网络的模型。并用来识别猫咪。代码在我的GIT的WEEK1中。最后达到的程度为
这个结果依然是高方差(过拟合)的问题。我们可以用正则化有效解决过拟合。

为什么正则化可以减小过拟合

过拟合的原因是因为某些神经元特征被过分放大,因为当这个特征被无限放大后成本函数效果非常好。那么就会一直激励这个异常的特征。为了消除这种影响。手段就是要削弱某些异常兴奋的神经元的影响。便引入了正则化项。这样可以让模型整体更加圆滑。不会因为某几个数据而过分拟合。(因为那几个兴奋神经元会在正则化因子的影响下,权重明显变低)

正则化因子设置的足够大的情况下,为了使成本函数最小化,权重矩阵 W 就会被设置为接近于 0 的值,直观上相当于消除了很多神经元的影响,那么大的神经网络就会变成一个较小的网络。当然,实际上隐藏层的神经元依然存在,但是其影响减弱了,便不会导致过拟合。

下面是数学解释

假设神经元中使用的激活函数为g(z) = tanh(z)(sigmoid 同理)。

image.png

在加入正则化项后,当 λ (正则化项的权重)增大,导致 W[l]减小,Z[l] = W[l]a[l-1]}+ b^[l]便会减小。由上图可知,在 z 较小(接近于 0)的区域里,tanh(z)函数近似线性,所以每层的函数就近似线性函数,整个网络就成为一个简单的近似线性的网络,因此不会发生过拟合。

之后会介绍的dropout法也是不要依赖于任何一个特征,因为该单元的输入可能随时被清除,因此该单元通过这种方式传播下去,通过传播所有权重,dropout将产生收缩权重的平方范数的效果,和之后介绍的L2正则化类似;实施dropout的结果实它会压缩权重,并完成一些预防过拟合的外层正则化;对不同权重的衰减是不同的,它取决于激活函数倍增的大小。

有哪些正则化

数据增强、L2 正则化(权重衰减)、L1 正则化、Dropout、Drop Connect、随机池化和早停

3.1 数据增强

数据增强是提升算法性能、满足深度学习模型对大量数据的需求的重要工具。数据增强通过向训练数据添加转换或扰动来人工增加训练数据集。数据增强技术如水平或垂直翻转图像、裁剪、色彩变换、扩展和旋转通常应用在视觉表象和图像分类中。

3.2 L1 和 L2 正则化

L1 和 L2 正则化是最常用的正则化方法。L1 正则化向目标函数添加正则化项,以减少参数的绝对值总和;而 L2 正则化中,添加正则化项的目的在于减少参数平方的总和。根据之前的研究,L1 正则化中的很多参数向量是稀疏向量,因为很多模型导致参数趋近于 0,因此它常用于特征选择设置中。机器学习中最常用的正则化方法是对权重施加 L2 范数约束。

标准正则化代价函数如下:

image.png

其中正则化项 R(w) 是:

image

另一种惩罚权重的绝对值总和的方法是 L1 正则化:

image

神经网络中的正则化如下:


image.png
这边有个很重要的一点

在上一章中,我们介绍了怎么根据损失函数一步一步用链式法则,推出各个系数的偏导。
但是正则化项是在加在成本函数上的。并且和M这个大小没有关系,我不能通过对一个损失函数的关系去链式法则反向传播回去那样。重新把有正则化情况下的偏导给求出来。所以这里用了一个技巧。因为正则项只涉及W这个参数
我们在求dW的时候,单独对那一层的正则项求导放进去。因为dW 在代码中已经是对所有M个样本求平均了,所以也和M无关。这样就可以把这个导数项,坐落在这个所有M个样本求完平均的dW上


image.png

image.png

最后我们对式子做一个变形。


image.png

就会发现,其实我们是每次反向传播都在原来的WL上乘以了一个【0,1】之间的系数
因此L2正则化也被称为“权重衰减”

3.3 dropout随机失活

除了正则化,还有一个非常实用的正则化方法——“Dropout(随机失活)”,我们来看看它的工作原理。

image

假设你在训练上图这样的神经网络,它存在过拟合,这就是dropout所要处理的,我们复制这个神经网络,dropout会遍历网络的每一层,并设置消除神经网络中节点的概率。假设网络中的每一层,每个节点都以抛硬币的方式设置概率,每个节点得以保留和消除的概率都是0.5,设置完节点概率,我们会消除一些节点,然后删除掉从该节点进出的连线,最后得到一个节点更少,规模更小的网络,然后用backprop方法进行训练。

image
image

这是网络节点精简后的一个样本,对于其它样本,我们照旧以抛硬币的方式设置概率,保留一类节点集合,删除其它类型的节点集合。对于每个训练样本,我们都将采用一个精简后神经网络来训练它,这种方法似乎有点怪,单纯遍历节点,编码也是随机的,可它真的有效。不过可想而知,我们针对每个训练样本训练规模极小的网络,最后你可能会认识到为什么要正则化网络,因为我们在训练极小的网络。

在测试阶段,我们并未使用dropout,自然也就不用抛硬币来决定失活概率,以及要消除哪些隐藏单元了,因为在测试阶段进行预测时,我们不期望输出结果是随机的,如果测试阶段应用dropout函数,预测会受到干扰。

3.4 early-stop法

早停法可以限制模型最小化代价函数所需的训练迭代次数。早停法通常用于防止训练中过度表达的模型泛化性能差。如果迭代次数太少,算法容易欠拟合(方差较小,偏差较大),而迭代次数太多,算法容易过拟合(方差较大,偏差较小)。早停法通过确定迭代次数解决这个问题,不需要对特定值进行手动设置。

如何使用在代码里,来看新的效果

我们先运用L2正则
代码改动如下


image.png

image.png

随后我们在上一章猫的分类器,运用不同的lambda来看下效果

image.png

输出如下

Cost after iteration 0: 0.771749
Cost after iteration 500: 0.540138
Cost after iteration 1000: 0.315187
Cost after iteration 1500: 0.161189
Cost after iteration 2000: 0.113821
0, accuracy 0.985645933014,0.8

Cost after iteration 0: 0.774388
Cost after iteration 500: 0.538099
Cost after iteration 1000: 0.288602
Cost after iteration 1500: 0.159615
Cost after iteration 2000: 0.117881
0.03, accuracy 0.985645933014,0.84

Cost after iteration 0: 0.780545
Cost after iteration 500: 0.543503
Cost after iteration 1000: 0.302193
Cost after iteration 1500: 0.171333
Cost after iteration 2000: 0.126968
0.1, accuracy 0.985645933014,0.82

Cost after iteration 0: 0.798136
Cost after iteration 500: 0.553439
Cost after iteration 1000: 0.331003
Cost after iteration 1500: 0.197385
Cost after iteration 2000: 0.152896
0.3, accuracy 0.980861244019,0.84

Cost after iteration 0: 0.859704
Cost after iteration 500: 0.656218
Cost after iteration 1000: 0.353583
Cost after iteration 1500: 0.232796
Cost after iteration 2000: 0.180967
1, accuracy 0.995215311005,0.82

Cost after iteration 0: 1.035612
Cost after iteration 500: 0.807824
Cost after iteration 1000: 0.534605
Cost after iteration 1500: 0.389707
Cost after iteration 2000: 0.323090
3, accuracy 1.0,0.78

Cost after iteration 0: 1.651292
Cost after iteration 500: 1.194095
Cost after iteration 1000: 0.904027
Cost after iteration 1500: 0.660152
Cost after iteration 2000: 0.549094
10, accuracy 1.0,0.78

Cost after iteration 0: 3.410376
Cost after iteration 500: 1.544157
Cost after iteration 1000: 0.963250
Cost after iteration 1500: 0.762616
Cost after iteration 2000: 0.688912
30, accuracy 0.655502392344,0.34

可以看到一开始还是缓解了过拟合的状态。
并没有达到最佳的效果,还是因为训练集数据量过小。
随着lambda增大,我们也看出后来使得模型表达力过于弱,把模型效果变成欠拟合。

下面我们会加上dropout层,然后分别实验L2正则, DROPOUT正则,和2个正则同时使用的效果。

在原来神经网络函数里,加入dropout层,我们需要把随机出来激活项目矩阵D 也要存进cache里,因为反向传播的时候还需要用。


image.png

反向传播的代码改动稍微复杂一些。因为当前层需要用到的dropout矩阵是前一层的D.所以我们需要enhance一些linear_cache


image.png

随后就是使用前一层dropout D 来更新dA

image.png

深入理解dropout为什么代码是这样工作的?

  1. 首先生成dropout 矩阵 乘以原来的A, 我们就使得每一个样本进来的时候,都会有一些神经元随机失效。为了不让原来的值变小,我们再除以这个概率。保证最后的A 的总体期望值和原来一致。这就是前向传播的代码意义。


    image.png
  2. 在反向传播的时候,我们的目标是算出前一层的dA, 那么因为前向传播用了一个遮罩把部分神经元变为0. 反向传播就应该用相同的遮罩回给前一层的dA也是这种效果
    就如下图,我们在第五层(输出层)反向传播需要用到的是之前第4层的Dropout 矩阵。 把这个选中来的时候的神经元回传。
  3. 最后我们为什么要在反向传播时也要除以dropout呢
    比如正向的函数是y = 3 * x
    经过dropout 我们变成了 y = 3 * x / 0.5 也就是 y = 6 * x
    所以再求导的时候回由原来的3变成6.
    但是这个信息并没有记录在W矩阵中。前向传播是直接改了最终结果(等价于下一层输入的A),所以反向传播的时候也要 除以 keep_prob, 把正确的导数应用回来。

最后就是一点小细节。因为数值过小,python的浮点数会有精度不足的问题。我们需要对原有的程序做一些优化。


image.png

image.png
image.png

最后我们可以加上同时应用dropout 和 L2的代码

# test both l2 and dropout regularization
parameters = L_layer_model(train_X, train_Y, layers_dims, lambd=0.3, keep_prob = 0.95, learning_rate = 0.3, num_iterations=30000, print_cost=10000)
# parameters = model(train_X, train_Y, keep_prob = 0.86, learning_rate = 0.3)
predictions_train, acc_train = predict(train_X, train_Y, parameters)
print ("On the train set:" + str(acc_train))
predictions_test, acc_test = predict(test_X, test_Y, parameters)
print ("On the test set:"+ str(acc_test))
draw("Model with both-regularization")

图像如下:


image.png

image.png
Cost after iteration 0: 0.673469
Cost after iteration 10000: 0.221898
Cost after iteration 20000: 0.211323
On the train set:0.9336492891
On the test set:0.96

比吴恩达作业中单独使用L2, 和单独使用dropout效果要更好。

第四步 明白梯度消失和爆炸,知道参数初始化和标准化的意义

我们知道 我们在选择特征的时候,可能每个特征的数据范围是不同的。那么我们运用相同的学习速率和同样大小的随机的初始参数。就会有一个问题。一边梯度下降已经走完了,另一边还有很长的路要走。所以比较好的做法就是把所有的输入特征X,都缩放到同一个范围。这样就可以用同一个学习速率去梯度下降。
当然我们在做测试的时候也要用相同的方法。


image.png

一般有4种特征缩放的方法


image.png

如果速度不一致的话,很可能有些参数已经下降完成了,而另一些参数还在下降的过程中,就像下图的左边的情况。(我的理解,如果不normalize,就要让每个参数有不一样的Learning rate,才能速度保持一致)


image.png

了解了什么是标准化和它的意义。
我们再来看一下什么是梯度消失和爆炸。

在梯度函数上出现的以指数级递增或者递减的情况分别称为梯度爆炸或者梯度消失。

我们可以看到下图就是,我们没有选对合适的初始化参数,造成cost在0.65就收敛了。(在第一周实验中,初始化函数默认用了*0.01,如果这个代码不改就会产生下图。后来吴恩达把这块改成了HE 也就是用了1 / sqrt( previous_layer_nn_count))而如果用了合适的初始化参数就可以更好的收敛。


image.png

下面我们就从正向传播和反向传播两个地方来理解,梯度是怎么消失的。

首先,假设我们初始化了比较大的参数会如何呢?

        parameters['W' + str(l)] = np.random.randn(layer_dims[l], layer_dims[l - 1]) * 10 + 10

随后为了说明问题,我们构建1个比较小的5层,神经网络

layers_dims = [2,2,2,2,1]

随后 我们打印 第一层的W和最后一层的W,以及只打印第0次迭代, 和 最后一次的迭代。
层与层连接的激活函数都用了sigmoid。
初始化如下

 'W1': 
array([[  5.81955359,   7.77784558],
       [ 12.3915114 ,   2.87202676]])
'W4': 
array([[ 14.91487884,   8.68389207]])

因为参数值打,造成正向传播后sigmoid的输出都会靠近1.
因为我们这层的输出是 sigmoid(WX + B)
那么这边的输出就成了下一层的输入,下一层的W依然很大。造成前向传播到最后就是清一色的1

forward 1: 
[[ 0.79571742  0.9862795   0.99836189  0.99999962  0.99583334  0.98620444
   1.          1.        ]
 [ 0.82147221  0.97409601  0.99568215  0.99992894  0.99921066  0.99955727
   1.          1.        ]]
forward 4:
 [[ 1.  1.  1.  1.  1.  1.  1.  1.]]

那么反向传播会如何呢?
每一层的W 都会用dw去更新
对最后一层来说dw4 = (a4 - y) * a3.T
倒数第二层是
dw3 = (a4-y) * w4.T * sigmoid'(z3) * a2.T
a2, a3都是清一色的1.
z3 是由a2 和 W3 乘出来的,所以z3 的大小和W3相同。
那么sigmoid的导数在很大的Z3下。是趋近于0的。
那么dw3/ dw4 = w4.T * 很小的数
下面我们只要知道 10 * sigmoid' (10) 是什么粒度的数即可。

首先sigmoid(10) = 0.99995
sigmoid'(x) = sigmoid(x) * (1 - sigmoid(x))
sigmoid'(10) = 0.00005 * 0.99995 =
0.00005
所以即使*10 = 0.0005 还是一个很小的数。
那么在向后传播的时候,每一层都会乘以一个这么小的系数
假设我们迭代1000次。
那么前面2层的W矩阵是几乎不会变的。
我们根据程序的输出来验证一下我们的猜想。

backward 4: [[ 0.375       0.37499948]]
backward 1: [[  1.19819348e-17   3.86127179e-18]

可以看到反向传播的DW,在第4层, 和 第一层的大小。
那么迭代1000次后的第一层系数矩阵变成了什么样了呢

old:
[[  5.81955359,   7.77784558],
 [ 12.3915114 ,   2.87202676]]
1000 times iteration new:
[[  5.81955359   7.77784558]
 [ 12.3915114    2.87202676]]

几乎没有任何变化。这就是梯度消失。

那么假设我们把初始化函数弄的很小,系数是怎么消失的呢?
我们修改初始化的代码如下:

parameters['W' + str(l)] = np.random.randn(layer_dims[l], layer_dims[l - 1]) * 0.01

如果很小的话,我们可以设想到根据SIGMOID函数的图像。那么每一层输出的值都会是0.5

forward 1: 
[[ 0.49942898  0.49833826  0.4975074   0.49459787  0.49758668  0.49787318
   0.48509335  0.48088925]
 [ 0.50034278  0.50045491  0.50068237  0.49998722  0.50190995  0.5026333
   0.4975558   0.49628407]]
forward 4: 
[[ 0.50148967  0.50148967  0.50148967  0.50148967  0.50148967  0.50148967
   0.50148967  0.50148967]]

那么根据我们上面推出的公式。
对最后一层来说dw4 = (a4 - y) * a3.T

倒数第二层是
dw3 = (a4-y) * w4.T * sigmoid'(z3) * a2.T
z3 = w3.T * a2
a2 = 0.5, w3.T 很小;
所以a2 = 0.005
sigmoid(0.005) = 0.5
这个时候sigmoid' = (1-0.5) * 0.5 似乎还是比较大的。
问题就出在w4.T 比较小上了。
所以我们来看下

backward 4: [[-0.06154413 -0.06180182]]
backward 1: [[  7.40484338e-10   1.15784792e-08]

虽然没有之前小的那么夸张,不过最后一层的更新幅度和第一层也是千差万别。

如果我们用推荐的He Initialization

parameters['W' + str(l)] = np.random.randn(layer_dims[l], layer_dims[l - 1]) * 2 / np.sqrt(layer_dims[l - 1])

可以看到,还是比较好的缓解了梯度消失的问题,虽然问题也是还是有。只是没有那么夸张。

backward 3: [[-0.15296214 -0.11593723]]
backward 0: [[-0.00047431  0.00655586]

似乎小伙们想问,无论是调大还是调小,似乎都只有梯度消失。而没有发生梯度爆炸。那么梯度爆炸是怎么来的。
那么我们就需要用relu这个激活函数。首先我们会随机初始化比较大的参数

        parameters['W' + str(l)] = np.random.randn(layer_dims[l], layer_dims[l - 1]) * 10 + 10

可以看到初始化的结果如下:

'W1': 
array([[  2.53955793,  16.39476215],
       [ 18.40681867,   0.9655079 ]])
'W4':
array([[ 10.75626351,  18.88741759]])

那么根据前向传播,因为每次都要迭代上权重矩阵。所以第4层 要比第一层肯定要大很多的。

forward 1: 
[[   1.89343201    7.06581645   10.59872467   27.24744262    6.68005048
     3.16321098   82.1120251   106.95812413]
 [   1.93723266    4.06756689    6.10135034    8.90754011    9.52202694
    11.14064199   17.61576178   20.90470549]]
forward 3: 
[[     0.             57.21442308     85.82163461    999.36071697      0.
       0.           4071.82741559   5538.44891721]
 [   864.09779824   2651.66989651   3977.50484476   8997.23699312
    3535.53583617   3114.46524666  25441.64488825  32770.06278669]]
forward 4: 
[[ 1.  1.  1.  1.  1.  1.  1.  1.]]

有些值为0,是因为涉及到负号的,就会被relu修正到0.
那么因为很大,所以输出就全为1了。
然后开始反向传播,同样,因为每次会用relu的导数,以及系数矩阵去乘上上层的输入。
而relu的导数是<=0的为0,否则为1.
所以
那么对第每层的W来说,就会减的特别大。

backward 4: 
[[   10.72770433  1328.43824095]]
backward 1: 
[[ 575.15932977  660.40865336]
 [ 682.43219276  369.60748073]]
W1: 
[[-54.97637504 -49.64610318]
 [-49.8364006  -35.99524017]]
W4: 
[[   9.68349307 -113.95640651]]

这样系数矩阵很容易就变为都是负的了。
一旦变为负值那么relu,就会收敛到0.

forward 1: 
[[ 0.  0.  0.  0.  0.  0.  0.  0.]
 [ 0.  0.  0.  0.  0.  0.  0.  0.]]
forward 3: 
[[ 0.  0.  0.  0.  0.  0.  0.  0.]
 [ 0.  0.  0.  0.  0.  0.  0.  0.]]
forward 4: 
[[ 0.4906261  0.4906261  0.4906261  0.4906261  0.4906261  0.4906261
   0.4906261  0.4906261]]
cost:0.698010451511
backward 4: 
[[ 0.  0.]]
backward 1: 
[[ 0.  0.]
 [ 0.  0.]]

就会一直收敛在cost:0.698010451511, 无法更新。这个就是梯度爆炸的问题。

2个实验代码(梯度消失和梯度爆炸),我都提交到我的github了,有兴趣的小伙伴,可以自己体验一下

第五步 学会用梯度检验来debug

梯度检验就是用的微积分的思想。我们在这个位置,利用一个小量,去计算斜率。然后看计算的斜率和我们算出来的梯度是否差不多。如果差不多则证明我们反向传播算法中算的梯度没有问题。


image.png
  1. 不要在训练中使用梯度检验,它只用于调试(debug)。使用完毕关闭梯度检验的功能;
  2. 如果算法的梯度检验失败,要检查所有项,并试着找出 bug,即确定哪个 dθapprox[i] 与 dθ 的值相差比较大;
  3. 当成本函数包含正则项时,也需要带上正则项进行检验;
  4. 梯度检验不能与 dropout 同时使用。因为每次迭代过程中,dropout 会随机消除隐藏层单元的不同子集,难以计算 dropout 在梯度下降上的成本函数 J。建议关闭 dropout,用梯度检验进行双重检查,确定在没有 dropout 的情况下算法正确,然后打开 dropout;

下面我们就在,我们的项目里,实现在自己的梯度检验。

首先,完成梯度检验函数的编码工作
核心逻辑为
前向传播中W,b的权重系数先打平,随后依次加上epsilon 和 减去epsilon
然后用前向传播算出COST
然后就可以算斜率了。
然后在和反向传播中算出来的db,dW做比较即可

import numpy as np

from week1.dnn_utils import compute_cost
from week2.gradientCheck.gc_utils import gradients_to_vector, dictionary_to_vector, vector_to_dictionary, \
    gradient_check_n_test_case
from week2.nn_backward_v2 import L_model_backward_reg
from week2.nn_forward_v2 import L_model_forward_reg


def forward_propagation_n(X, Y, parameters, lambd = 0):
    AL, caches = L_model_forward_reg(X, parameters, 1)
    cost = compute_cost(AL, Y, parameters, lambd)
    return cost, caches, AL


def gradient_check_n(parameters, gradients, X, Y, epsilon=1e-7, lambd = 0):
    """
    Checks if backward_propagation_n computes correctly the gradient of the cost output by forward_propagation_n

    Arguments:
    parameters -- python dictionary containing your parameters "W1", "b1", "W2", "b2", "W3", "b3":
    grad -- output of backward_propagation_n, contains gradients of the cost with respect to the parameters. 
    x -- input datapoint, of shape (input size, 1)
    y -- true "label"
    epsilon -- tiny shift to the input to compute approximated gradient with formula(1)

    Returns:
    difference -- difference (2) between the approximated gradient and the backward propagation gradient
    """
    # Set-up variables
    parameters_values, _, shapes = dictionary_to_vector(parameters)
    grad = gradients_to_vector(gradients)
    num_parameters = parameters_values.shape[0]
    J_plus = np.zeros((num_parameters, 1))
    J_minus = np.zeros((num_parameters, 1))
    gradapprox = np.zeros((num_parameters, 1))

    # Compute gradapprox
    for i in range(num_parameters):

        thetaplus = np.copy(parameters_values)  # Step 1
        thetaplus[i][0] = thetaplus[i][0] + epsilon  # Step 2
        J_plus[i], _, _ = forward_propagation_n(X, Y, vector_to_dictionary(thetaplus, parameters.keys(),shapes), lambd)  # 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, parameters.keys(),shapes), lambd)  # Step 3
        # Compute gradapprox[i]
        gradapprox[i] = (J_plus[i] - J_minus[i]) / 2 / epsilon

    # Compare gradapprox to backward propagation gradients by computing difference.
    numerator = np.linalg.norm(grad - gradapprox)  # Step 1'
    denominator = np.linalg.norm(grad) + np.linalg.norm(gradapprox)  # Step 2'
    difference = numerator / denominator  # Step 3'

    if difference > 2e-7:
        print(
            "\033[93m" + "There is a mistake in the backward propagation! difference = " + str(difference) + "\033[0m")
    else:
        print(
            "\033[92m" + "Your backward propagation works perfectly fine! difference = " + str(difference) + "\033[0m")

    return difference

然后把GC 需要用的辅助函数类写好

import numpy as np

def dictionary_to_vector(parameters):
    """
    Roll all our parameters dictionary into a single vector satisfying our specific required shape.
    """
    keys = []
    count = 0
    shapes = {}
    for key in sorted(parameters):

        # flatten parameter
        shapes[key] = parameters[key].shape
        new_vector = np.reshape(parameters[key], (-1, 1))
        keys = keys + [key] * new_vector.shape[0]

        if count == 0:
            theta = new_vector
        else:
            theta = np.concatenate((theta, new_vector), axis=0)
        count = count + 1

    return theta, keys, shapes


def vector_to_dictionary(theta, keys, shapes):
    """
    Unroll all our parameters dictionary from a single vector satisfying our specific required shape.
    """
    parameters = {}
    idx = 0
    for key in sorted(keys):
        next = idx + shapes[key][0] * shapes[key][1]
        parameters[key] = theta[idx:next].reshape(shapes[key])
        idx = next
    return parameters


def gradients_to_vector(gradients):
    """
    Roll all our gradients dictionary into a single vector satisfying our specific required shape.
    """
    count = 0
    for key in sorted(gradients.keys()):
        if not (key.startswith('dW') or key.startswith('db')):
            continue
        # flatten parameter
        new_vector = np.reshape(gradients[key], (-1, 1))

        if count == 0:
            theta = new_vector
        else:
            theta = np.concatenate((theta, new_vector), axis=0)
        count = count + 1

    return theta

随后我们就可以在我们的netural_network_v2.py 的框架里多加一个,是否要打开梯度检验的参数。


image.png

然后再我们的APP中使用


image.png

总结

在这次专题中,

  1. 我们理解训练集,验证集和测试集,在海量数据,和普通数据中应该如何划分。
  2. 理解了偏差和方差的问题
  3. 学会了2种解决高方差问题的手段, 正则化和dropout并用代码实现
  4. 明白了梯度消失和梯度爆炸是怎么在算法流程中被触发的。明白了初始化参数的重要性
  5. 学会了梯度检验DEBUG法。

你可能感兴趣的:(吴恩达 改善深层神经网络:超参数调试、正则化以及优化)