精华内容
下载资源
问答
  • 吴恩达课后编程作业】Course 1 - 神经网络和深度学习 - 第二周作业 - 具有神经网络思维的Logistic回归 上一篇:【课程1 - 第二周测验】※※※※※ 【回到目录】※※※※※下一篇:【课程1 - 第三周测验】 ...

    【吴恩达课后编程作业】Course 1 - 神经网络和深度学习 - 第二周作业 - 具有神经网络思维的Logistic回归


    上一篇:【课程1 - 第二周测验】※※※※※ 【回到目录】※※※※※下一篇:【课程1 - 第三周测验】

    在开始之前,首先声明本文参考【Kulbear】的github上的文章,本文参考Logistic Regression with a Neural Network mindset,我基于他的文章加以自己的理解发表这篇博客,力求让大家以最轻松的姿态理解吴恩达的视频,如有不妥的地方欢迎大家指正。


    本文所使用的资料已上传到百度网盘【点击下载】,提取码:2u3w ,请在开始之前下载好所需资料,然后将文件解压到你的代码文件同一级目录下,请确保你的代码那里有lr_utils.py和datasets文件夹。


    【博主使用的python版本:3.6.2】


    我们要做的事是搭建一个能够**【识别猫】** 的简单的神经网络,你可以跟随我的步骤在Jupyter Notebook中一步步地把代码填进去,也可以直接复制完整代码,在完整代码在本文最底部。

    在开始之前,我们有需要引入的库:

    • numpy :是用Python进行科学计算的基本软件包。
    • h5py:是与H5文件中存储的数据集进行交互的常用软件包。
    • matplotlib:是一个著名的库,用于在Python中绘制图表。
    • lr_utils :在本文的资料包里,一个加载资料包里面的数据的简单功能的库。

    如果你没有以上的库,请自行安装。

    import numpy as np
    import matplotlib.pyplot as plt
    import h5py
    from lr_utils import load_dataset
    
    

    lr_utils.py代码如下,你也可以自行打开它查看:

    import numpy as np
    import h5py
        
        
    def load_dataset():
        train_dataset = h5py.File('datasets/train_catvnoncat.h5', "r")
        train_set_x_orig = np.array(train_dataset["train_set_x"][:]) # your train set features
        train_set_y_orig = np.array(train_dataset["train_set_y"][:]) # your train set labels
    
        test_dataset = h5py.File('datasets/test_catvnoncat.h5', "r")
        test_set_x_orig = np.array(test_dataset["test_set_x"][:]) # your test set features
        test_set_y_orig = np.array(test_dataset["test_set_y"][:]) # your test set labels
    
        classes = np.array(test_dataset["list_classes"][:]) # the list of classes
        
        train_set_y_orig = train_set_y_orig.reshape((1, train_set_y_orig.shape[0]))
        test_set_y_orig = test_set_y_orig.reshape((1, test_set_y_orig.shape[0]))
        
        return train_set_x_orig, train_set_y_orig, test_set_x_orig, test_set_y_orig, classes
    

    解释以下上面的load_dataset() 返回的值的含义:

    • train_set_x_orig :保存的是训练集里面的图像数据(本训练集有209张64x64的图像)。
    • train_set_y_orig :保存的是训练集的图像对应的分类值(【0 | 1】,0表示不是猫,1表示是猫)。
    • test_set_x_orig :保存的是测试集里面的图像数据(本训练集有50张64x64的图像)。
    • test_set_y_orig : 保存的是测试集的图像对应的分类值(【0 | 1】,0表示不是猫,1表示是猫)。
    • classes : 保存的是以bytes类型保存的两个字符串数据,数据为:[b’non-cat’ b’cat’]。

    现在我们就要把这些数据加载到主程序里面:

    train_set_x_orig , train_set_y , test_set_x_orig , test_set_y , classes = load_dataset()
    

    我们可以看一下我们加载的文件里面的图片都是些什么样子的,比如我就查看一下训练集里面的第26张图片,当然你也可以改变index的值查看一下其他的图片。

    index = 25
    plt.imshow(train_set_x_orig[index])
    #print("train_set_y=" + str(train_set_y)) #你也可以看一下训练集里面的标签是什么样的。
    

    运行结果如下:
    index_25

    现在我们可以结合一下训练集里面的数据来看一下我到底都加载了一些什么东西。

    #打印出当前的训练标签值
    #使用np.squeeze的目的是压缩维度,【未压缩】train_set_y[:,index]的值为[1] , 【压缩后】np.squeeze(train_set_y[:,index])的值为1
    #print("【使用np.squeeze:" + str(np.squeeze(train_set_y[:,index])) + ",不使用np.squeeze: " + str(train_set_y[:,index]) + "】")
    #只有压缩后的值才能进行解码操作
    print("y=" + str(train_set_y[:,index]) + ", it's a " + classes[np.squeeze(train_set_y[:,index])].decode("utf-8") + "' picture")
    

    打印出的结果是:y=[1], it's a cat' picture,我们进行下一步,我们查看一下我们加载的图像数据集具体情况,我对以下参数做出解释:

    • m_train :训练集里图片的数量。
    • m_test :测试集里图片的数量。
    • num_px : 训练、测试集里面的图片的宽度和高度(均为64x64)。

    请记住,train_set_x_orig 是一个维度为(m_​​train,num_px,num_px,3)的数组。

    m_train = train_set_y.shape[1] #训练集里图片的数量。
    m_test = test_set_y.shape[1] #测试集里图片的数量。
    num_px = train_set_x_orig.shape[1] #训练、测试集里面的图片的宽度和高度(均为64x64)。
    
    #现在看一看我们加载的东西的具体情况
    print ("训练集的数量: m_train = " + str(m_train))
    print ("测试集的数量 : m_test = " + str(m_test))
    print ("每张图片的宽/高 : num_px = " + str(num_px))
    print ("每张图片的大小 : (" + str(num_px) + ", " + str(num_px) + ", 3)")
    print ("训练集_图片的维数 : " + str(train_set_x_orig.shape))
    print ("训练集_标签的维数 : " + str(train_set_y.shape))
    print ("测试集_图片的维数: " + str(test_set_x_orig.shape))
    print ("测试集_标签的维数: " + str(test_set_y.shape))
    

    运行之后的结果:

    训练集的数量: m_train = 209
    测试集的数量 : m_test = 50
    每张图片的宽/: num_px = 64
    每张图片的大小 : (64, 64, 3)
    训练集_图片的维数 : (209, 64, 64, 3)
    训练集_标签的维数 : (1, 209)
    测试集_图片的维数: (50, 64, 64, 3)
    测试集_标签的维数: (1, 50)
    

    为了方便,我们要把维度为(64,64,3)的numpy数组重新构造为(64 x 64 x 3,1)的数组,要乘以3的原因是每张图片是由64x64像素构成的,而每个像素点由(R,G,B)三原色构成的,所以要乘以3。在此之后,我们的训练和测试数据集是一个numpy数组,【每列代表一个平坦的图像】 ,应该有m_train和m_test列。

    当你想将形状(a,b,c,d)的矩阵X平铺成形状(b * c * d,a)的矩阵X_flatten时,可以使用以下代码:

    #X_flatten = X.reshape(X.shape [0],-1).T #X.T是X的转置
    #将训练集的维度降低并转置。
    train_set_x_flatten  = train_set_x_orig.reshape(train_set_x_orig.shape[0],-1).T
    #将测试集的维度降低并转置。
    test_set_x_flatten = test_set_x_orig.reshape(test_set_x_orig.shape[0], -1).T
    

    这一段意思是指把数组变为209行的矩阵(因为训练集里有209张图片),但是我懒得算列有多少,于是我就用-1告诉程序你帮我算,最后程序算出来时12288列,我再最后用一个T表示转置,这就变成了12288行,209列。测试集亦如此。

    然后我们看看降维之后的情况是怎么样的:

    print ("训练集降维最后的维度: " + str(train_set_x_flatten.shape))
    print ("训练集_标签的维数 : " + str(train_set_y.shape))
    print ("测试集降维之后的维度: " + str(test_set_x_flatten.shape))
    print ("测试集_标签的维数 : " + str(test_set_y.shape))
    

    执行之后的结果为:

    训练集降维最后的维度: (12288, 209)
    训练集_标签的维数 : (1, 209)
    测试集降维之后的维度: (12288, 50)
    测试集_标签的维数 : (1, 50)
    

    为了表示彩色图像,必须为每个像素指定红色,绿色和蓝色通道(RGB),因此像素值实际上是从0到255范围内的三个数字的向量。机器学习中一个常见的预处理步骤是对数据集进行居中和标准化,这意味着可以减去每个示例中整个numpy数组的平均值,然后将每个示例除以整个numpy数组的标准偏差。但对于图片数据集,它更简单,更方便,几乎可以将数据集的每一行除以255(像素通道的最大值),因为在RGB中不存在比255大的数据,所以我们可以放心的除以255,让标准化的数据位于[0,1]之间,现在标准化我们的数据集:

    train_set_x = train_set_x_flatten / 255
    test_set_x = test_set_x_flatten / 255
    

    flatten


    现在总算是把我们加载的数据弄完了,我们现在开始构建神经网络。

    以下是数学表达式,如果对数学公式不甚理解,请仔细看一下吴恩达的视频。

    对于 x(i)x^{(i)}: z(i)=wTx(i)+b(1)z^{(i)} = w^T x^{(i)} + b \tag{1}y^(i)=a(i)=sigmoid(z(i))(2)\hat{y}^{(i)} = a^{(i)} = sigmoid(z^{(i)})\tag{2}L(a(i),y(i))=y(i)log(a(i))(1y(i))log(1a(i))(3) \mathcal{L}(a^{(i)}, y^{(i)}) = - y^{(i)} \log(a^{(i)}) - (1-y^{(i)} ) \log(1-a^{(i)})\tag{3}

    然后通过对所有训练样例求和来计算成本: J=1mi=1mL(a(i),y(i))(4) J = \frac{1}{m} \sum_{i=1}^m \mathcal{L}(a^{(i)}, y^{(i)})\tag{4}

    建立神经网络的主要步骤是:

    1. 定义模型结构(例如输入特征的数量)

    2. 初始化模型的参数

    3. 循环:

      3.1 计算当前损失(正向传播)

      3.2 计算当前梯度(反向传播)

      3.3 更新参数(梯度下降)

    现在构建sigmoid(),需要使用 sigmoid(w ^ T x + b) 计算来做出预测。

    def sigmoid(z):
        """
        参数:
            z  - 任何大小的标量或numpy数组。
        
        返回:
            s  -  sigmoid(z)
        """
        s = 1 / (1 + np.exp(-z))
        return s
    

    我们可以测试一下sigmoid(),检查一下是否符合我们所需要的条件。

    #测试sigmoid()
    print("====================测试sigmoid====================")
    print ("sigmoid(0) = " + str(sigmoid(0)))
    print ("sigmoid(9.2) = " + str(sigmoid(9.2)))
    

    打印出的结果为:

    ====================测试sigmoid====================
    sigmoid(0) = 0.5
    sigmoid(9.2) = 0.999898970806
    

    既然sigmoid测试好了,我们现在就可以初始化我们需要的参数w和b了。

    def initialize_with_zeros(dim):
        """
            此函数为w创建一个维度为(dim,1)的0向量,并将b初始化为0。
            
            参数:
                dim  - 我们想要的w矢量的大小(或者这种情况下的参数数量)
            
            返回:
                w  - 维度为(dim,1)的初始化向量。
                b  - 初始化的标量(对应于偏差)
        """
        w = np.zeros(shape = (dim,1))
        b = 0
        #使用断言来确保我要的数据是正确的
        assert(w.shape == (dim, 1)) #w的维度是(dim,1)
        assert(isinstance(b, float) or isinstance(b, int)) #b的类型是float或者是int
        
        return (w , b)
    

    初始化参数的函数已经构建好了,现在就可以执行“前向”和“后向”传播步骤来学习参数。

    我们现在要实现一个计算成本函数及其渐变的函数propagate()。

    def propagate(w, b, X, Y):
    	"""
        实现前向和后向传播的成本函数及其梯度。
        参数:
            w  - 权重,大小不等的数组(num_px * num_px * 3,1)
            b  - 偏差,一个标量
            X  - 矩阵类型为(num_px * num_px * 3,训练数量)
            Y  - 真正的“标签”矢量(如果非猫则为0,如果是猫则为1),矩阵维度为(1,训练数据数量)
    
        返回:
            cost- 逻辑回归的负对数似然成本
            dw  - 相对于w的损失梯度,因此与w相同的形状
            db  - 相对于b的损失梯度,因此与b的形状相同
        """
    	m = X.shape[1]
        
        #正向传播
        A = sigmoid(np.dot(w.T,X) + b) #计算激活值,请参考公式2。
        cost = (- 1 / m) * np.sum(Y * np.log(A) + (1 - Y) * (np.log(1 - A))) #计算成本,请参考公式3和4。
        
        #反向传播
        dw = (1 / m) * np.dot(X, (A - Y).T) #请参考视频中的偏导公式。
        db = (1 / m) * np.sum(A - Y) #请参考视频中的偏导公式。
    	
    	#使用断言确保我的数据是正确的
        assert(dw.shape == w.shape)
        assert(db.dtype == float)
        cost = np.squeeze(cost)
        assert(cost.shape == ())
        
        #创建一个字典,把dw和db保存起来。
        grads = {
                    "dw": dw,
                    "db": db
                 }
        return (grads , cost)
    

    写好之后我们来测试一下。

    #测试一下propagate
    print("====================测试propagate====================")
    #初始化一些参数
    w, b, X, Y = np.array([[1], [2]]), 2, np.array([[1,2], [3,4]]), np.array([[1, 0]])
    grads, cost = propagate(w, b, X, Y)
    print ("dw = " + str(grads["dw"]))
    print ("db = " + str(grads["db"]))
    print ("cost = " + str(cost))
    

    测试结果是:

    ====================测试propagate====================
    dw = [[ 0.99993216]
     [ 1.99980262]]
    db = 0.499935230625
    cost = 6.00006477319
    

    现在,我要使用渐变下降更新参数。

    目标是通过最小化成本函数 JJ 来学习 wwbb 。对于参数 θ\theta ,更新规则是 $ \theta = \theta - \alpha \text{ } d\theta$,其中 α\alpha 是学习率。

    def optimize(w , b , X , Y , num_iterations , learning_rate , print_cost = False):
        """
        此函数通过运行梯度下降算法来优化w和b
        
        参数:
            w  - 权重,大小不等的数组(num_px * num_px * 3,1)
            b  - 偏差,一个标量
            X  - 维度为(num_px * num_px * 3,训练数据的数量)的数组。
            Y  - 真正的“标签”矢量(如果非猫则为0,如果是猫则为1),矩阵维度为(1,训练数据的数量)
            num_iterations  - 优化循环的迭代次数
            learning_rate  - 梯度下降更新规则的学习率
            print_cost  - 每100步打印一次损失值
        
        返回:
            params  - 包含权重w和偏差b的字典
            grads  - 包含权重和偏差相对于成本函数的梯度的字典
            成本 - 优化期间计算的所有成本列表,将用于绘制学习曲线。
        
        提示:
        我们需要写下两个步骤并遍历它们:
            1)计算当前参数的成本和梯度,使用propagate()。
            2)使用w和b的梯度下降法则更新参数。
        """
        
        costs = []
        
        for i in range(num_iterations):
            
            grads, cost = propagate(w, b, X, Y)
            
            dw = grads["dw"]
            db = grads["db"]
            
            w = w - learning_rate * dw
            b = b - learning_rate * db
            
            #记录成本
            if i % 100 == 0:
                costs.append(cost)
            #打印成本数据
            if (print_cost) and (i % 100 == 0):
                print("迭代的次数: %i , 误差值: %f" % (i,cost))
            
        params  = {
                    "w" : w,
                    "b" : b }
        grads = {
                "dw": dw,
                "db": db } 
        return (params , grads , costs)
    

    现在就让我们来测试一下优化函数:

    #测试optimize
    print("====================测试optimize====================")
    w, b, X, Y = np.array([[1], [2]]), 2, np.array([[1,2], [3,4]]), np.array([[1, 0]])
    params , grads , costs = optimize(w , b , X , Y , num_iterations=100 , learning_rate = 0.009 , print_cost = False)
    print ("w = " + str(params["w"]))
    print ("b = " + str(params["b"]))
    print ("dw = " + str(grads["dw"]))
    print ("db = " + str(grads["db"]))
    

    测试结果为:

    ====================测试optimize====================
    w = [[ 0.1124579 ]
     [ 0.23106775]]
    b = 1.55930492484
    dw = [[ 0.90158428]
     [ 1.76250842]]
    db = 0.430462071679
    

    optimize函数会输出已学习的w和b的值,我们可以使用w和b来预测数据集X的标签。

    现在我们要实现预测函数predict()。计算预测有两个步骤:

    1. 计算 Y^=A=σ(wTX+b)\hat{Y} = A = \sigma(w^T X + b)

    2. 将a的值变为0(如果激活值<= 0.5)或者为1(如果激活值> 0.5),

    然后将预测值存储在向量Y_prediction中。

    def predict(w , b , X ):
        """
        使用学习逻辑回归参数logistic (w,b)预测标签是0还是1,
        
        参数:
            w  - 权重,大小不等的数组(num_px * num_px * 3,1)
            b  - 偏差,一个标量
            X  - 维度为(num_px * num_px * 3,训练数据的数量)的数据
        
        返回:
            Y_prediction  - 包含X中所有图片的所有预测【0 | 1】的一个numpy数组(向量)
        
        """
        
        m  = X.shape[1] #图片的数量
        Y_prediction = np.zeros((1,m)) 
        w = w.reshape(X.shape[0],1)
        
        #计预测猫在图片中出现的概率
        A = sigmoid(np.dot(w.T , X) + b)
        for i in range(A.shape[1]):
            #将概率a [0,i]转换为实际预测p [0,i]
            Y_prediction[0,i] = 1 if A[0,i] > 0.5 else 0
        #使用断言
        assert(Y_prediction.shape == (1,m))
        
        return Y_prediction
    

    老规矩,测试一下。

    #测试predict
    print("====================测试predict====================")
    w, b, X, Y = np.array([[1], [2]]), 2, np.array([[1,2], [3,4]]), np.array([[1, 0]])
    print("predictions = " + str(predict(w, b, X)))
    

    就目前而言,我们基本上把所有的东西都做完了,现在我们要把这些函数统统整合到一个model()函数中,届时只需要调用一个model()就基本上完成所有的事了。

    def model(X_train , Y_train , X_test , Y_test , num_iterations = 2000 , learning_rate = 0.5 , print_cost = False):
        """
        通过调用之前实现的函数来构建逻辑回归模型
        
        参数:
            X_train  - numpy的数组,维度为(num_px * num_px * 3,m_train)的训练集
            Y_train  - numpy的数组,维度为(1,m_train)(矢量)的训练标签集
            X_test   - numpy的数组,维度为(num_px * num_px * 3,m_test)的测试集
            Y_test   - numpy的数组,维度为(1,m_test)的(向量)的测试标签集
            num_iterations  - 表示用于优化参数的迭代次数的超参数
            learning_rate  - 表示optimize()更新规则中使用的学习速率的超参数
            print_cost  - 设置为true以每100次迭代打印成本
        
        返回:
            d  - 包含有关模型信息的字典。
        """
        w , b = initialize_with_zeros(X_train.shape[0])
        
        parameters , grads , costs = optimize(w , b , X_train , Y_train,num_iterations , learning_rate , print_cost)
        
        #从字典“参数”中检索参数w和b
        w , b = parameters["w"] , parameters["b"]
        
        #预测测试/训练集的例子
        Y_prediction_test = predict(w , b, X_test)
        Y_prediction_train = predict(w , b, X_train)
        
        #打印训练后的准确性
        print("训练集准确性:"  , format(100 - np.mean(np.abs(Y_prediction_train - Y_train)) * 100) ,"%")
        print("测试集准确性:"  , format(100 - np.mean(np.abs(Y_prediction_test - Y_test)) * 100) ,"%")
        
        d = {
                "costs" : costs,
                "Y_prediction_test" : Y_prediction_test,
                "Y_prediciton_train" : Y_prediction_train,
                "w" : w,
                "b" : b,
                "learning_rate" : learning_rate,
                "num_iterations" : num_iterations }
        return d
    

    把整个model构建好之后我们这就算是正式的实际测试了,我们这就来实际跑一下。

    print("====================测试model====================")     
    #这里加载的是真实的数据,请参见上面的代码部分。
    d = model(train_set_x, train_set_y, test_set_x, test_set_y, num_iterations = 2000, learning_rate = 0.005, print_cost = True)
    

    执行后的数据:

    ====================测试model====================
    迭代的次数: 0 , 误差值: 0.693147
    迭代的次数: 100 , 误差值: 0.584508
    迭代的次数: 200 , 误差值: 0.466949
    迭代的次数: 300 , 误差值: 0.376007
    迭代的次数: 400 , 误差值: 0.331463
    迭代的次数: 500 , 误差值: 0.303273
    迭代的次数: 600 , 误差值: 0.279880
    迭代的次数: 700 , 误差值: 0.260042
    迭代的次数: 800 , 误差值: 0.242941
    迭代的次数: 900 , 误差值: 0.228004
    迭代的次数: 1000 , 误差值: 0.214820
    迭代的次数: 1100 , 误差值: 0.203078
    迭代的次数: 1200 , 误差值: 0.192544
    迭代的次数: 1300 , 误差值: 0.183033
    迭代的次数: 1400 , 误差值: 0.174399
    迭代的次数: 1500 , 误差值: 0.166521
    迭代的次数: 1600 , 误差值: 0.159305
    迭代的次数: 1700 , 误差值: 0.152667
    迭代的次数: 1800 , 误差值: 0.146542
    迭代的次数: 1900 , 误差值: 0.140872
    训练集准确性: 99.04306220095694 %
    测试集准确性: 70.0 %
    

    我们更改一下学习率和迭代次数,有可能会发现训练集的准确性可能会提高,但是测试集准确性会下降,这是由于过拟合造成的,但是我们并不需要担心,我们以后会使用更好的算法来解决这些问题的。


    到目前为止,我们的程序算是完成了,但是,我们可以在后面加一点东西,比如画点图什么的。

    #绘制图
    costs = np.squeeze(d['costs'])
    plt.plot(costs)
    plt.ylabel('cost')
    plt.xlabel('iterations (per hundreds)')
    plt.title("Learning rate =" + str(d["learning_rate"]))
    plt.show()
    

    跑一波出来的效果图是这样的,可以看到成本下降,它显示参数正在被学习:
    pic

    让我们进一步分析一下,并研究学习率alpha的可能选择。为了让渐变下降起作用,我们必须明智地选择学习速率。学习率α\alpha 决定了我们更新参数的速度。如果学习率过高,我们可能会“超过”最优值。同样,如果它太小,我们将需要太多迭代才能收敛到最佳值。这就是为什么使用良好调整的学习率至关重要的原因。

    我们可以比较一下我们模型的学习曲线和几种学习速率的选择。也可以尝试使用不同于我们初始化的learning_rates变量包含的三个值,并看一下会发生什么。

    learning_rates = [0.01, 0.001, 0.0001]
    models = {}
    for i in learning_rates:
        print ("learning rate is: " + str(i))
        models[str(i)] = model(train_set_x, train_set_y, test_set_x, test_set_y, num_iterations = 1500, learning_rate = i, print_cost = False)
        print ('\n' + "-------------------------------------------------------" + '\n')
    
    for i in learning_rates:
        plt.plot(np.squeeze(models[str(i)]["costs"]), label= str(models[str(i)]["learning_rate"]))
    
    plt.ylabel('cost')
    plt.xlabel('iterations')
    
    legend = plt.legend(loc='upper center', shadow=True)
    frame = legend.get_frame()
    frame.set_facecolor('0.90')
    plt.show()
    

    跑一下打印出来的结果是:

    learning rate is: 0.01
    训练集准确性: 99.52153110047847 %
    测试集准确性: 68.0 %
    
    -------------------------------------------------------
    
    learning rate is: 0.001
    训练集准确性: 88.99521531100478 %
    测试集准确性: 64.0 %
    
    -------------------------------------------------------
    
    learning rate is: 0.0001
    训练集准确性: 68.42105263157895 %
    测试集准确性: 36.0 %
    
    -------------------------------------------------------
    

    pic2


    **【完整代码】: **

    # -*- coding: utf-8 -*-
    """
    Created on Wed Mar 21 17:25:30 2018
    
    博客地址 :http://blog.csdn.net/u013733326/article/details/79639509
    
    @author: Oscar
    """
    
    import numpy as np
    import matplotlib.pyplot as plt
    import h5py
    from lr_utils import load_dataset
    
    train_set_x_orig , train_set_y , test_set_x_orig , test_set_y , classes = load_dataset()
    
    m_train = train_set_y.shape[1] #训练集里图片的数量。
    m_test = test_set_y.shape[1] #测试集里图片的数量。
    num_px = train_set_x_orig.shape[1] #训练、测试集里面的图片的宽度和高度(均为64x64)。
    
    #现在看一看我们加载的东西的具体情况
    print ("训练集的数量: m_train = " + str(m_train))
    print ("测试集的数量 : m_test = " + str(m_test))
    print ("每张图片的宽/高 : num_px = " + str(num_px))
    print ("每张图片的大小 : (" + str(num_px) + ", " + str(num_px) + ", 3)")
    print ("训练集_图片的维数 : " + str(train_set_x_orig.shape))
    print ("训练集_标签的维数 : " + str(train_set_y.shape))
    print ("测试集_图片的维数: " + str(test_set_x_orig.shape))
    print ("测试集_标签的维数: " + str(test_set_y.shape))
    
    #将训练集的维度降低并转置。
    train_set_x_flatten  = train_set_x_orig.reshape(train_set_x_orig.shape[0],-1).T
    #将测试集的维度降低并转置。
    test_set_x_flatten = test_set_x_orig.reshape(test_set_x_orig.shape[0], -1).T
    
    print ("训练集降维最后的维度: " + str(train_set_x_flatten.shape))
    print ("训练集_标签的维数 : " + str(train_set_y.shape))
    print ("测试集降维之后的维度: " + str(test_set_x_flatten.shape))
    print ("测试集_标签的维数 : " + str(test_set_y.shape))
    
    train_set_x = train_set_x_flatten / 255
    test_set_x = test_set_x_flatten / 255
    
    def sigmoid(z):
        """
        参数:
            z  - 任何大小的标量或numpy数组。
    
        返回:
            s  -  sigmoid(z)
        """
        s = 1 / (1 + np.exp(-z))
        return s
    
    def initialize_with_zeros(dim):
        """
            此函数为w创建一个维度为(dim,1)的0向量,并将b初始化为0。
    
            参数:
                dim  - 我们想要的w矢量的大小(或者这种情况下的参数数量)
    
            返回:
                w  - 维度为(dim,1)的初始化向量。
                b  - 初始化的标量(对应于偏差)
        """
        w = np.zeros(shape = (dim,1))
        b = 0
        #使用断言来确保我要的数据是正确的
        assert(w.shape == (dim, 1)) #w的维度是(dim,1)
        assert(isinstance(b, float) or isinstance(b, int)) #b的类型是float或者是int
    
        return (w , b)
    
    def propagate(w, b, X, Y):
        """
        实现前向和后向传播的成本函数及其梯度。
        参数:
            w  - 权重,大小不等的数组(num_px * num_px * 3,1)
            b  - 偏差,一个标量
            X  - 矩阵类型为(num_px * num_px * 3,训练数量)
            Y  - 真正的“标签”矢量(如果非猫则为0,如果是猫则为1),矩阵维度为(1,训练数据数量)
    
        返回:
            cost- 逻辑回归的负对数似然成本
            dw  - 相对于w的损失梯度,因此与w相同的形状
            db  - 相对于b的损失梯度,因此与b的形状相同
        """
        m = X.shape[1]
    
        #正向传播
        A = sigmoid(np.dot(w.T,X) + b) #计算激活值,请参考公式2。
        cost = (- 1 / m) * np.sum(Y * np.log(A) + (1 - Y) * (np.log(1 - A))) #计算成本,请参考公式3和4。
    
        #反向传播
        dw = (1 / m) * np.dot(X, (A - Y).T) #请参考视频中的偏导公式。
        db = (1 / m) * np.sum(A - Y) #请参考视频中的偏导公式。
    
        #使用断言确保我的数据是正确的
        assert(dw.shape == w.shape)
        assert(db.dtype == float)
        cost = np.squeeze(cost)
        assert(cost.shape == ())
    
        #创建一个字典,把dw和db保存起来。
        grads = {
                    "dw": dw,
                    "db": db
                 }
        return (grads , cost)
    
    def optimize(w , b , X , Y , num_iterations , learning_rate , print_cost = False):
        """
        此函数通过运行梯度下降算法来优化w和b
    
        参数:
            w  - 权重,大小不等的数组(num_px * num_px * 3,1)
            b  - 偏差,一个标量
            X  - 维度为(num_px * num_px * 3,训练数据的数量)的数组。
            Y  - 真正的“标签”矢量(如果非猫则为0,如果是猫则为1),矩阵维度为(1,训练数据的数量)
            num_iterations  - 优化循环的迭代次数
            learning_rate  - 梯度下降更新规则的学习率
            print_cost  - 每100步打印一次损失值
    
        返回:
            params  - 包含权重w和偏差b的字典
            grads  - 包含权重和偏差相对于成本函数的梯度的字典
            成本 - 优化期间计算的所有成本列表,将用于绘制学习曲线。
    
        提示:
        我们需要写下两个步骤并遍历它们:
            1)计算当前参数的成本和梯度,使用propagate()。
            2)使用w和b的梯度下降法则更新参数。
        """
    
        costs = []
    
        for i in range(num_iterations):
    
            grads, cost = propagate(w, b, X, Y)
    
            dw = grads["dw"]
            db = grads["db"]
    
            w = w - learning_rate * dw
            b = b - learning_rate * db
    
            #记录成本
            if i % 100 == 0:
                costs.append(cost)
            #打印成本数据
            if (print_cost) and (i % 100 == 0):
                print("迭代的次数: %i , 误差值: %f" % (i,cost))
    
        params  = {
                    "w" : w,
                    "b" : b }
        grads = {
                "dw": dw,
                "db": db } 
        return (params , grads , costs)
    
    def predict(w , b , X ):
        """
        使用学习逻辑回归参数logistic (w,b)预测标签是0还是1,
    
        参数:
            w  - 权重,大小不等的数组(num_px * num_px * 3,1)
            b  - 偏差,一个标量
            X  - 维度为(num_px * num_px * 3,训练数据的数量)的数据
    
        返回:
            Y_prediction  - 包含X中所有图片的所有预测【0 | 1】的一个numpy数组(向量)
    
        """
    
        m  = X.shape[1] #图片的数量
        Y_prediction = np.zeros((1,m)) 
        w = w.reshape(X.shape[0],1)
    
        #计预测猫在图片中出现的概率
        A = sigmoid(np.dot(w.T , X) + b)
        for i in range(A.shape[1]):
            #将概率a [0,i]转换为实际预测p [0,i]
            Y_prediction[0,i] = 1 if A[0,i] > 0.5 else 0
        #使用断言
        assert(Y_prediction.shape == (1,m))
    
        return Y_prediction
    
    def model(X_train , Y_train , X_test , Y_test , num_iterations = 2000 , learning_rate = 0.5 , print_cost = False):
        """
        通过调用之前实现的函数来构建逻辑回归模型
    
        参数:
            X_train  - numpy的数组,维度为(num_px * num_px * 3,m_train)的训练集
            Y_train  - numpy的数组,维度为(1,m_train)(矢量)的训练标签集
            X_test   - numpy的数组,维度为(num_px * num_px * 3,m_test)的测试集
            Y_test   - numpy的数组,维度为(1,m_test)的(向量)的测试标签集
            num_iterations  - 表示用于优化参数的迭代次数的超参数
            learning_rate  - 表示optimize()更新规则中使用的学习速率的超参数
            print_cost  - 设置为true以每100次迭代打印成本
    
        返回:
            d  - 包含有关模型信息的字典。
        """
        w , b = initialize_with_zeros(X_train.shape[0])
    
        parameters , grads , costs = optimize(w , b , X_train , Y_train,num_iterations , learning_rate , print_cost)
    
        #从字典“参数”中检索参数w和b
        w , b = parameters["w"] , parameters["b"]
    
        #预测测试/训练集的例子
        Y_prediction_test = predict(w , b, X_test)
        Y_prediction_train = predict(w , b, X_train)
    
        #打印训练后的准确性
        print("训练集准确性:"  , format(100 - np.mean(np.abs(Y_prediction_train - Y_train)) * 100) ,"%")
        print("测试集准确性:"  , format(100 - np.mean(np.abs(Y_prediction_test - Y_test)) * 100) ,"%")
    
        d = {
                "costs" : costs,
                "Y_prediction_test" : Y_prediction_test,
                "Y_prediciton_train" : Y_prediction_train,
                "w" : w,
                "b" : b,
                "learning_rate" : learning_rate,
                "num_iterations" : num_iterations }
        return d
    
    d = model(train_set_x, train_set_y, test_set_x, test_set_y, num_iterations = 2000, learning_rate = 0.005, print_cost = True)
    
    #绘制图
    costs = np.squeeze(d['costs'])
    plt.plot(costs)
    plt.ylabel('cost')
    plt.xlabel('iterations (per hundreds)')
    plt.title("Learning rate =" + str(d["learning_rate"]))
    plt.show()
    
    展开全文
  • 【中文】【吴恩达课后编程作业】Course 4 - 卷积神经网络 - 第四周作业 - 人脸识别与神经风格转换

    【中文】【吴恩达课后编程作业】Course 4 - 卷积神经网络 - 第四周作业 - 人脸识别与神经风格转换


    上一篇:【课程4 - 第四周测验】※※※※※ 【回到目录】※※※※※下一篇:【待撰写-课程5 - 第一周测验】

    资料下载

    • 本文所使用的资料已上传到百度网盘【点击下载(555.65MB)】,提取码:zcjp ,请在开始之前下载好所需资料,底部不提供代码。

    【博主使用的python版本:3.6.2】


    第一部分 - 人脸识别


    给之前的“欢乐家”添加人脸识别系统

    这是第4周的编程作业,在这里你将构建一个人脸识别系统。这里的许多想法来自FaceNet。在课堂中,吴恩达老师也讨论了 DeepFace

    人脸识别系统通常被分为两大类:

    • 人脸验证:“这是不是本人呢?”,比如说,在某些机场你能够让系统扫描您的面部并验证您是否为本人从而使得您免人工检票通过海关,又或者某些手机能够使用人脸解锁功能。这些都是1:1匹配问题。

    • 人脸识别:“这个人是谁?”,比如说,在视频中的百度员工进入办公室时的脸部识别视频的介绍,无需使用另外的ID卡。这个是1:K的匹配问题。

     FaceNet可以将人脸图像编码为一个128位数字的向量从而进行学习,通过比较两个这样的向量,那么我们就可以确定这两张图片是否是属于同一个人。

    在本节中,你将学到:

    • 实现三元组损失函数。

    • 使用一个已经训练好了的模型来将人脸图像映射到一个128位数字的的向量。

    • 使用这些编码来执行人脸验证和人脸识别。

     在此次练习中,我们使用一个训练好了的模型,该模型使用了“通道优先”的约定来代表卷积网络的激活,而不是在视频中和以前的编程作业中使用的“通道最后”的约定。换句话说,数据的维度是(m,nC,nH,nW)(m,n_C,n_H,n_W)而不是(m,nH,nW,nC)(m,n_H,n_W,n_C),这两种约定在开源实现中都有一定的吸引力,但是在深度学习的社区中还没有统一的标准。

    我们先来导入需要的包:

    from keras.models import Sequential
    from keras.layers import Conv2D, ZeroPadding2D, Activation, Input, concatenate
    from keras.models import Model
    from keras.layers.normalization import BatchNormalization
    from keras.layers.pooling import MaxPooling2D, AveragePooling2D
    from keras.layers.merge import Concatenate
    from keras.layers.core import Lambda, Flatten, Dense
    from keras.initializers import glorot_uniform
    from keras.engine.topology import Layer
    from keras import backend as K
    
    #------------用于绘制模型细节,可选--------------#
    from IPython.display import SVG
    from keras.utils.vis_utils import model_to_dot
    from keras.utils import plot_model
    #------------------------------------------------#
    
    K.set_image_data_format('channels_first')
    
    import time
    import cv2
    import os
    import numpy as np
    from numpy import genfromtxt
    import pandas as pd
    import tensorflow as tf
    import fr_utils
    from inception_blocks_v2 import *
    
    %matplotlib inline
    %load_ext autoreload
    %autoreload 2
    
    np.set_printoptions(threshold=np.nan)
    

    0 - 简单的人脸验证

     在人脸验证中,你需要给出两张照片并想知道是否是同一个人,最简单的方法是逐像素地比较这两幅图像,如果图片之间的误差小于选择的阈值,那么则可能是同一个人。

    **图 1**
     当然,如果你真的这么做的话效果一定会很差,因为像素值的变化在很大程度上是由于光照、人脸的朝向、甚至头部的位置的微小变化等等。接下来与使用原始图像不同的是我们可以让系统学习构建一个编码$f(img)$,对该编码的元素进行比较,可以更准确地判断两幅图像是否属于同一个人。

    1 - 将人脸图像编码为128位的向量

    1.1 - 使用卷积网络来进行编码

     FaceNet模型需要大量的数据和长时间的训练,因为,遵循在应用深度学习设置中常见的实践,我们要加载其他人已经训练过的权值。在网络的架构上我们遵循Szegedy et al.等人的初始模型。这里我们提供了初始模型的实现方法,你可以打开inception_blocks.py文件来查看是如何实现的。

     关键信息如下:

    • 该网络使用了96×9696 \times 96的RGB图像作为输入数据,图像数量为mm,输入的数据维度为(m,nc,nh,nw)=(m,3,96,96)(m,n_c,n_h,n_w) = (m,3,96,96).

    • 输出为(m,128)(m,128)的已经编码的mm128128位的向量。

    我们可以运行下面的代码来创建一个人脸识别的模型。

    #获取模型
    FRmodel = faceRecoModel(input_shape=(3,96,96))
    
    #打印模型的总参数数量
    print("参数数量:" + str(FRmodel.count_params()))
    

    执行结果:

    参数数量:3743280
    

    我们可以绘制出模型细节(可选):

    #------------用于绘制模型细节,可选--------------#
    %matplotlib inline
    plot_model(FRmodel, to_file='FRmodel.png')
    SVG(model_to_dot(FRmodel).create(prog='dot', format='svg'))
    #------------------------------------------------#
    

    执行结果: 结果请详见**文章最底部**或者资料中的“FRmodel.png”

     通过使用128神经元全连接层作为最后一层,该模型确保输出是大小为128的编码向量,然后使用比较两个人脸图像的编码如下:

    **图 2**:
    通过计算两个编码和阈值之间的误差,可以确定这两幅图是否代表同一个人。

     因此,如果满足下面两个条件的话,编码是一个比较好的方法:

    • 同一个人的两个图像的编码非常相似。

    • 两个不同人物的图像的编码非常不同。

     三元组损失函数将上面的形式实现,它会试图将同一个人的两个图像(对于给定的图和正例)的编码“拉近”,同时将两个不同的人的图像(对于给定的图和负例)进一步“分离”。


    **图 3**:
    在下一部分中,我们将从左到右调用图片: Anchor (A), Positive (P), Negative (N)

    1.3 - 三元组损失函数

     对于给定的图像xx,其编码为f(x)f(x),其中ff为神经网络的计算函数。

    我们将使用三元组图像APN(A,P,N)进行训练:

    • AA是“Anchor”,是一个人的图像。

    • PP是“Positive”,是相对于“Anchor”的同一个人的另外一张图像。

    • NN是“Negative”,是相对于“Anchor”的不同的人的另外一张图像。

     这些三元组来自训练集,我们使用(A(i),P(i),N(i))(A^{(i)},P^{(i)},N^{(i)})来表示第ii个训练样本。我们要保证图像A(i)A^{(i)}与图像P(i)P^{(i)}的差值至少比与图像N(i)N^{(i)}的差值相差α\alpha

    f(A(i))f(P(i))22+α<f(A(i))f(N(i))22(1)\mid \mid f(A^{(i)}) - f(P^{(i)}) \mid \mid_2^2 + \alpha \quad < \quad \mid \mid f(A^{(i)}) - f(N^{(i)}) \mid \mid_2^2 \tag{1}

     我们希望让三元组损失变为最小:

    J=i=1m[f(A(i))f(P(i))22(2)f(A(i))f(N(i))22(3)+α]+(4)\mathcal{J} = \sum^{m}_{i=1} \large[ \small \underbrace{\mid \mid f(A^{(i)}) - f(P^{(i)}) \mid \mid_2^2}_\text{(2)} - \underbrace{\mid \mid f(A^{(i)}) - f(N^{(i)}) \mid \mid_2^2}_\text{(3)} + \alpha \large ] \small_+ \tag{4}

    • 在这里,我们使用“[]+[···]_+”来表示函数max(z,0)max(z,0)

    需要注意的是:

    • 公式(2)是给定三元组AA与正例PP之间的距离的平方,我们要让它变小。

    • 公式(3)是给定三元组AA与负例NN之间的距离的平方,我们要让它变大,经公式(1)变换后前面偶一个负号。

    • α\alpha是间距,这个需要我们来手动选择,这里我们使用α=0.2\alpha = 0.2

     大多数实现将编码归一化为范数等于1的向量,即(f(img)2\mid \mid f(img)\mid \mid_2=1),这里你没必要操心这个。现在我们要实现公式(4),由以下4步构成:

    1. 计算"anchor" 与 "positive"之间编码的距离:f(A(i))f(P(i))22\mid \mid f(A^{(i)}) - f(P^{(i)}) \mid \mid_2^2

    2. 计算"anchor" 与 "negative"之间编码的距离:f(A(i))f(N(i))22\mid \mid f(A^{(i)}) - f(N^{(i)}) \mid \mid_2^2

    3. 根据公式计算每个样本的值:$ \mid \mid f(A^{(i)}) - f(P^{(i)}) \mid - \mid \mid f(A^{(i)}) - f(N^{(i)}) \mid \mid_2^2 + \alpha$

    4. 通过取带零的最大值和对训练样本的求和来计算整个公式:J=i=1m[f(A(i))f(P(i))22f(A(i))f(N(i))22+α]+(4)\mathcal{J} = \sum^{m}_{i=1} \large[ \small \mid \mid f(A^{(i)}) - f(P^{(i)}) \mid \mid_2^2 - \mid \mid f(A^{(i)}) - f(N^{(i)}) \mid \mid_2^2+ \alpha \large ] \small_+ \tag{4}

     一些会用到的函数:tf.reduce_sum()tf.square()tf.subtract()tf.add(), tf.maximum(),对于步骤1与步骤2,需要对f(A(i))f(P(i))22\mid \mid f(A^{(i)}) - f(P^{(i)}) \mid \mid_2^2f(A(i))f(N(i))22\mid \mid f(A^{(i)}) - f(N^{(i)}) \mid \mid_2^2的其中的项进行求和,对于步骤4,你需要对整个训练集进行求和。

    def triplet_loss(y_true, y_pred, alpha = 0.2):
        """
        根据公式(4)实现三元组损失函数
        
        参数:
            y_true -- true标签,当你在Keras里定义了一个损失函数的时候需要它,但是这里不需要。
            y_pred -- 列表类型,包含了如下参数:
                anchor -- 给定的“anchor”图像的编码,维度为(None,128)
                positive -- “positive”图像的编码,维度为(None,128)
                negative -- “negative”图像的编码,维度为(None,128)
            alpha -- 超参数,阈值
        
        返回:
            loss -- 实数,损失的值
        """
        #获取anchor, positive, negative的图像编码
        anchor, positive, negative = y_pred[0], y_pred[1], y_pred[2]
        
        #第一步:计算"anchor" 与 "positive"之间编码的距离,这里需要使用axis=-1
        pos_dist = tf.reduce_sum(tf.square(tf.subtract(anchor,positive)),axis=-1)
        
        #第二步:计算"anchor" 与 "negative"之间编码的距离,这里需要使用axis=-1
        neg_dist = tf.reduce_sum(tf.square(tf.subtract(anchor,negative)),axis=-1)
        
        #第三步:减去之前的两个距离,然后加上alpha
        basic_loss = tf.add(tf.subtract(pos_dist,neg_dist),alpha)
        
        #通过取带零的最大值和对训练样本的求和来计算整个公式
        loss = tf.reduce_sum(tf.maximum(basic_loss,0))
        
        return loss
    

    我们来测试一下:

    with tf.Session() as test:
        tf.set_random_seed(1)
        y_true = (None, None, None)
        y_pred = (tf.random_normal([3, 128], mean=6, stddev=0.1, seed = 1),
                  tf.random_normal([3, 128], mean=1, stddev=1, seed = 1),
                  tf.random_normal([3, 128], mean=3, stddev=4, seed = 1))
        loss = triplet_loss(y_true, y_pred)
        
        print("loss = " + str(loss.eval()))
    

    测试结果:

    loss = 528.143
    

    2 - 加载训练好了的模型

     FaceNet是通过最小化三元组损失来训练的,但是由于训练需要大量的数据和时间,所以我们不会从头训练,相反,我们会加载一个已经训练好了的模型,运行下列代码来加载模型,可能会需要几分钟的时间。

    #开始时间
    start_time = time.clock()
    
    #编译模型
    FRmodel.compile(optimizer = 'adam', loss = triplet_loss, metrics = ['accuracy'])
    
    #加载权值
    fr_utils.load_weights_from_FaceNet(FRmodel)
    
    #结束时间
    end_time = time.clock()
    
    #计算时差
    minium = end_time - start_time
    
    print("执行了:" + str(int(minium / 60)) + "分" + str(int(minium%60)) + "秒")
    

    执行结果:

    执行了:1分48秒
    

    这里有一些3个人之间的编码距离的例子:


    **图 4**:
    三个人的编码之间的距离的输出示例
    现在我们使用这个模型进行人脸验证和人脸识别。

    3 - 模型的应用

     之前我们对“欢乐家”添加了笑脸识别,现在我们要构建一个面部验证系统,以便只允许来自指定列表的人员进入。为了通过门禁,每个人都必须在门口刷身份证以表明自己的身份,然后人脸识别系统将检查他们到底是谁。

    3.1 - 人脸验证

     我们构建一个数据库,里面包含了允许进入的人员的编码向量,我们使用fr_uitls.img_to_encoding(image_path, model)函数来生成编码,它会根据图像来进行模型的前向传播。
     我们这里的数据库使用的是一个字典来表示,这个字典将每个人的名字映射到他们面部的128维编码上。

    database = {}
    database["danielle"] = fr_utils.img_to_encoding("images/danielle.png", FRmodel)
    database["younes"] = fr_utils.img_to_encoding("images/younes.jpg", FRmodel)
    database["tian"] = fr_utils.img_to_encoding("images/tian.jpg", FRmodel)
    database["andrew"] = fr_utils.img_to_encoding("images/andrew.jpg", FRmodel)
    database["kian"] = fr_utils.img_to_encoding("images/kian.jpg", FRmodel)
    database["dan"] = fr_utils.img_to_encoding("images/dan.jpg", FRmodel)
    database["sebastiano"] = fr_utils.img_to_encoding("images/sebastiano.jpg", FRmodel)
    database["bertrand"] = fr_utils.img_to_encoding("images/bertrand.jpg", FRmodel)
    database["kevin"] = fr_utils.img_to_encoding("images/kevin.jpg", FRmodel)
    database["felix"] = fr_utils.img_to_encoding("images/felix.jpg", FRmodel)
    database["benoit"] = fr_utils.img_to_encoding("images/benoit.jpg", FRmodel)
    database["arnaud"] = fr_utils.img_to_encoding("images/arnaud.jpg", FRmodel)
    

     现在,当有人出现在你的门前刷他们的身份证的时候,你可以在数据库中查找他们的编码,用它来检查站在门前的人是否与身份证上的名字匹配。

     现在我们要实现 verify() 函数来验证摄像头的照片(image_path)是否与身份证上的名称匹配,这个部分可由以下步骤构成:

    1. 根据image_path来计算编码。

    2. 计算与存储在数据库中的身份图像的编码的差距。

    3. 如果差距小于0.7,那么就打开门,否则就不开门。

     如上所述,我们使用L2(np.linalg.norm)来计算差距。(注意:在本实现中,将L2的误差(而不是L2误差的平方)与阈值0.7进行比较。)

    def verify(image_path, identity, database, model):
        """
        对“identity”与“image_path”的编码进行验证。
        
        参数:
            image_path -- 摄像头的图片。
            identity -- 字符类型,想要验证的人的名字。
            database -- 字典类型,包含了成员的名字信息与对应的编码。
            model -- 在Keras的模型的实例。
            
        返回:
            dist -- 摄像头的图片与数据库中的图片的编码的差距。
            is_open_door -- boolean,是否该开门。
        """
        #第一步:计算图像的编码,使用fr_utils.img_to_encoding()来计算。
        encoding = fr_utils.img_to_encoding(image_path, model)
        
        #第二步:计算与数据库中保存的编码的差距
        dist = np.linalg.norm(encoding - database[identity])
        
        #第三步:判断是否打开门
        if dist < 0.7:
            print("欢迎 " + str(identity) + "回家!")
            is_door_open = True
        else:
            print("经验证,您与" + str(identity) + "不符!")
            is_door_open = False
        
        return dist, is_door_open
    

     现在younes在门外,相机已经拍下了照片并存放在了(“images/camera_0.jpg”),现在我们来验证一下~

    verify("images/camera_0.jpg","younes",database,FRmodel)
    

    执行结果:

    欢迎 younes回家!
    (0.65939206, True)
    

     Benoit已经被禁止进入,也从数据库中删除了自己的信息,他偷了Kian的身份证并试图通过门禁,我们来看看他能不能进入呢?

    verify("images/camera_2.jpg", "kian", database, FRmodel)
    

    执行结果:

    经验证,您与kian不符!
    (0.86224037, False)
    

    3.2 - 人脸识别

     面部验证系统基本运行良好,但是自从Kian的身份证被偷后,那天晚上他回到房子那里就不能进去了!为了减少这种恶作剧,你想把你的面部验证系统升级成面部识别系统。这样就不用再带身份证了,一个被授权的人只要走到房子前面,前门就会自动为他们打开!

     我们将实现一个人脸识别系统,该系统将图像作为输入,并确定它是否是授权人员之一(如果是,是谁),与之前的人脸验证系统不同,我们不再将一个人的名字作为输入的一部分。

     现在我们要实现who_is_it()函数,实现它需要有以下步骤:

    1. 根据image_path计算图像的编码。

    2. 从数据库中找出与目标编码具有最小差距的编码。

      • 初始化min_dist变量为足够大的数字(100),它将找到与输入的编码最接近的编码。
      • 遍历数据库中的名字与编码,可以使用for (name, db_enc) in database.items()语句。
        • 计算目标编码与当前数据库编码之间的L2差距。
        • 如果差距小于min_dist,那么就更新名字与编码到identity与min_dist中。
    def who_is_it(image_path, database,model):
        """
        根据指定的图片来进行人脸识别
        
        参数:
            images_path -- 图像地址
            database -- 包含了名字与编码的字典
            model -- 在Keras中的模型的实例。
            
        返回:
            min_dist -- 在数据库中与指定图像最相近的编码。
            identity -- 字符串类型,与min_dist编码相对应的名字。
        """
        #步骤1:计算指定图像的编码,使用fr_utils.img_to_encoding()来计算。
        encoding = fr_utils.img_to_encoding(image_path, model)
        
        #步骤2 :找到最相近的编码
        ## 初始化min_dist变量为足够大的数字,这里设置为100
        min_dist = 100
        
        ## 遍历数据库找到最相近的编码
        for (name,db_enc) in database.items():
            ### 计算目标编码与当前数据库编码之间的L2差距。
            dist = np.linalg.norm(encoding - db_enc)
            
            ### 如果差距小于min_dist,那么就更新名字与编码到identity与min_dist中。
            if dist < min_dist:
                min_dist = dist
                identity = name
        
        # 判断是否在数据库中
        if min_dist > 0.7:
            print("抱歉,您的信息不在数据库中。")
            
        else:
            print("姓名" + str(identity) + "  差距:" + str(min_dist))
        
        return min_dist, identity
    

     Younes站在前门,相机给他拍了张照片(“images/camera_0.jpg”)。让我们看看who_it_is()算法是否识别Younes。

    who_is_it("images/camera_0.jpg", database, FRmodel)
    

    执行结果:

    姓名younes  差距:0.659392
    (0.65939206, 'younes')
    

    请记住:

    • 人脸验证解决了更容易的1:1匹配问题,人脸识别解决了更难的1∶k匹配问题。

    • 三重损失是训练神经网络学习人脸图像编码的一种有效的损失函数。

    • 相同的编码可用于验证和识别。测量两个图像编码之间的距离可以确定它们是否是同一个人的图片。


    第二部分 - 神经风格转换


    深度学习在艺术上的应用:神经风格转换

     这是本周的第二个编程作业,在这里我们将学习到如何进行神经风格转换,这个算法是Gatys创立的(2015,https://arxiv.org/abs/1508.06576 )。

    在这里,我们将:

    • 实现神经风格转换算法

    • 用算法生成新的艺术图像

     在之前的学习中我们都是优化了一个成本函数来获得一组参数值,在这里我们将优化成本函数以获取像素值,我们先来导入包:

    import time
    import os
    import sys
    import scipy.io
    import scipy.misc
    import matplotlib.pyplot as plt
    from matplotlib.pyplot import imshow
    from PIL import Image
    import nst_utils
    import numpy as np
    import tensorflow as tf
    
    %matplotlib inline
    

    1 - 问题描述

     神经风格转换(Neural Style Transfer,NST)是深学习中最有趣的技术之一。如下图所示,它合并两个图像,即“内容”图像(CContent)和“风格”图像(SStyle),以创建“生成的”图像(GGenerated)。生成的图像G将图像C的“内容”与图像S的“风格”相结合。

     在这个例子中,你将生成一个巴黎卢浮宫博物馆(内容图像C)与一个领袖印象派运动克劳德·莫奈的画(风格图像S)混合起来的绘画。

    我们来看看到底该怎样做~

    2 - 迁移学习

     神经风格转换(NST)使用先前训练好了的卷积网络,并在此基础之上进行构建。使用在不同任务上训练的网络并将其应用于新任务的想法称为迁移学习。

     根据原始的NST论文(https://arxiv.org/abs/1508.06576 ),我们将使用VGG网络,具体地说,我们将使用VGG-19,这是VGG网络的19层版本。这个模型已经在非常大的ImageNet数据库上进行了训练,因此学会了识别各种低级特征(浅层)和高级特征(深层)。

     运行以下代码从VGG模型加载参数。这可能需要几秒钟的时间。

    model = nst_utils.load_vgg_model("pretrained-model/imagenet-vgg-verydeep-19.mat")
    
    print(model)
    

    执行结果:

    {'input': <tf.Variable 'Variable:0' shape=(1, 300, 400, 3) dtype=float32_ref>, 'conv1_1': <tf.Tensor 'Relu:0' shape=(1, 300, 400, 64) dtype=float32>, 'conv1_2': <tf.Tensor 'Relu_1:0' shape=(1, 300, 400, 64) dtype=float32>, 'avgpool1': <tf.Tensor 'AvgPool:0' shape=(1, 150, 200, 64) dtype=float32>, 'conv2_1': <tf.Tensor 'Relu_2:0' shape=(1, 150, 200, 128) dtype=float32>, 'conv2_2': <tf.Tensor 'Relu_3:0' shape=(1, 150, 200, 128) dtype=float32>, 'avgpool2': <tf.Tensor 'AvgPool_1:0' shape=(1, 75, 100, 128) dtype=float32>, 'conv3_1': <tf.Tensor 'Relu_4:0' shape=(1, 75, 100, 256) dtype=float32>, 'conv3_2': <tf.Tensor 'Relu_5:0' shape=(1, 75, 100, 256) dtype=float32>, 'conv3_3': <tf.Tensor 'Relu_6:0' shape=(1, 75, 100, 256) dtype=float32>, 'conv3_4': <tf.Tensor 'Relu_7:0' shape=(1, 75, 100, 256) dtype=float32>, 'avgpool3': <tf.Tensor 'AvgPool_2:0' shape=(1, 38, 50, 256) dtype=float32>, 'conv4_1': <tf.Tensor 'Relu_8:0' shape=(1, 38, 50, 512) dtype=float32>, 'conv4_2': <tf.Tensor 'Relu_9:0' shape=(1, 38, 50, 512) dtype=float32>, 'conv4_3': <tf.Tensor 'Relu_10:0' shape=(1, 38, 50, 512) dtype=float32>, 'conv4_4': <tf.Tensor 'Relu_11:0' shape=(1, 38, 50, 512) dtype=float32>, 'avgpool4': <tf.Tensor 'AvgPool_3:0' shape=(1, 19, 25, 512) dtype=float32>, 'conv5_1': <tf.Tensor 'Relu_12:0' shape=(1, 19, 25, 512) dtype=float32>, 'conv5_2': <tf.Tensor 'Relu_13:0' shape=(1, 19, 25, 512) dtype=float32>, 'conv5_3': <tf.Tensor 'Relu_14:0' shape=(1, 19, 25, 512) dtype=float32>, 'conv5_4': <tf.Tensor 'Relu_15:0' shape=(1, 19, 25, 512) dtype=float32>, 'avgpool5': <tf.Tensor 'AvgPool_4:0' shape=(1, 10, 13, 512) dtype=float32>}
    

     该模型存储在一个python字典中,其中每个变量名都是键,相应的值是一个包含该变量值的张量,要通过此网络运行图像,只需将图像提供给模型。 在TensorFlow中,你可以使用tf.assign函数来做到这一点:

    #tf.assign函数用法
    model["input"].assign(image)
    

     这将图像作为输入给模型,在此之后,如果想要访问某个特定层的激活,比如4_2,请这样做:

    #访问 4_2 层的激活
    sess.run(model["conv4_2"])
    

    3 - 神经风格转换

     我们可以使用下面3个步骤来构建神经风格转换(Neural Style Transfer,NST)算法:

    • 构建内容损失函数Jcontent(C,G)J_{content}(C,G)

    • 构建风格损失函数Jstyle(S,G)J_{style}(S,G)

    • 把它放在一起得到J(G)=αJcontent(C,G)+βJstyle(S,G)J(G) = \alpha J_{content}(C,G) + \beta J_{style}(S,G).

    3.1 - 计算内容损失

     在我们的运行的例子中,内容图像C是巴黎卢浮宫博物馆的图片,运行下面的代码来看看卢浮宫的图片:

    content_image = scipy.misc.imread("images/louvre.jpg")
    imshow(content_image)
    

    内容图片©显示了卢浮宫的金字塔被旧的巴黎建筑包围,图片上还有阳光灿烂的天空和一些云彩。

    3.1.1 - 如何确保生成的图像G与图像C的内容匹配?

     正如我们在视频中看到的,浅层的一个卷积网络往往检测到较低层次的特征,如边缘和简单的纹理,更深层往往检测更高层次的特征,如更复杂的纹理以及对象分类等。

     我们希望“生成的”图像G具有与输入图像C相似的内容。假设我们选择了一些层的激活来表示图像的内容,在实践中,如果你在网络中间选择一个层——既不太浅也不太深,你会得到最好的的视觉结果。(当你完成了这个练习后,你可以用不同的图层进行实验,看看结果是如何变化的。)

     假设你选择了一个特殊的隐藏层,现在,将图像C作为已经训练好的VGG网络的输入,然后进行前向传播。让a(C)a^{(C)}成为你选择的层中的隐藏层激活(在视频中吴恩达老师写作a[l](C)a^{[l](C)},但在这里我们将去掉上标[l][l]以简化符号),激活值为nH×nW×nCn_H \times n_W \times n_C的张量。然后用图像G重复这个过程:将G设置为输入数据,并进行前向传播,让a(G)a^{(G)}成为相应的隐层激活,我们将把内容成本函数定义为:

    Jcontent(C,G)=14× nH×nW×nC(a(C)a(G))2(1)J_{content}(C,G) = \frac{1}{4 \times \ n_H \times n_W \times n_C }\sum_{所有条目}(a^{(C)} - a^{(G)})^2 \tag{1}

    博主注:14\frac{1}{4}的出处请详见视频4.9内容代价函数视频,于2:24处,吴恩达老师提及到“也可以在前面加上归一化或者不加,比如12\frac{1}{2}或者其他的,都影响不大”。

     这里nH,nW,nCn_H, n_W, n_C分别代表了你选择的隐藏层的高度、宽度、通道数,并出现在成本的归一化项中,为了使得方便理解,需要注意的是a(C)a^{(C)} 与 $ a^{(G)}$ 是与隐藏层激活相对应的卷积值,为了计算成本Jcontent(C,G)J_{content}(C,G),可以方便地将这些3D卷积展开为2D矩阵,如下所示。(从技术上讲,不需要这个展开步骤来计算JcontentJ_{content},但是当您以后需要执行类似的操作来计算JstyleJ_{style}时,这将是一个很好的实践。)

    现在我们要使用tensorflow来实现内容代价函数,它由以下3步构成:

    1. 从a_G中获取维度信息:

      • 从张量X中获取维度信息,可以使用:X.get_shape().as_list()
    2. 将a_C与a_G如上图一样降维:

    3. 计算内容代价:

    def compute_content_cost(a_C, a_G):
        """
        计算内容代价的函数
        
        参数:
            a_C -- tensor类型,维度为(1, n_H, n_W, n_C),表示隐藏层中图像C的内容的激活值。
            a_G -- tensor类型,维度为(1, n_H, n_W, n_C),表示隐藏层中图像G的内容的激活值。
        
        返回:
            J_content -- 实数,用上面的公式1计算的值。
            
        """
        
        #获取a_G的维度信息
        m, n_H, n_W, n_C = a_G.get_shape().as_list()
        
        #对a_C与a_G从3维降到2维
        a_C_unrolled = tf.transpose(tf.reshape(a_C, [n_H * n_W, n_C]))
        a_G_unrolled = tf.transpose(tf.reshape(a_G, [n_H * n_W, n_C]))
        
        #计算内容代价
        #J_content = (1 / (4 * n_H * n_W * n_C)) * tf.reduce_sum(tf.square(tf.subtract(a_C_unrolled, a_G_unrolled)))
        J_content = 1/(4*n_H*n_W*n_C)*tf.reduce_sum(tf.square(tf.subtract(a_C_unrolled, a_G_unrolled)))
        return J_content
    

    我们来测试一下:

    tf.reset_default_graph()
    
    with tf.Session() as test:
        tf.set_random_seed(1)
        a_C = tf.random_normal([1, 4, 4, 3], mean=1, stddev=4)
        a_G = tf.random_normal([1, 4, 4, 3], mean=1, stddev=4)
        J_content = compute_content_cost(a_C, a_G)
        print("J_content = " + str(J_content.eval()))
        
        test.close()
    

    测试结果:

    J_content = 6.76559
    

    需要记住的是:

    • 内容成本采用神经网络的隐层激活,并测量a(C)a^{(C)}a(G)a^{(G)}的区别。

    • 当我们以后最小化内容成本时,这将有助于确保GG的内容与CC相似。

    3.2 - 计算风格损失

    我们先来看一下下面的风格图片:

    style_image = scipy.misc.imread("images/monet_800600.jpg")
    
    imshow(style_image)
    

    执行结果:
    <img src="data:image/png;base64,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

    展开全文
  • 吴恩达课后编程作业】Course 1 - 神经网络和深度学习 - 第三周作业 - 带有一个隐藏层的平面数据分类 上一篇:【 课程1 - 第三周测验】※※※※※ 【回到目录】※※※※※下一篇:【课程1 - 第四周测验】 ...

    #【吴恩达课后编程作业】Course 1 - 神经网络和深度学习 - 第三周作业 - 带有一个隐藏层的平面数据分类


    上一篇:【 课程1 - 第三周测验】※※※※※ 【回到目录】※※※※※下一篇:【课程1 - 第四周测验】

    声明

       首先声明本文参考【Kulbear】的github上的文章,本文参考Planar data classification with one hidden layer,我基于他的文章加以自己的理解发表这篇博客,力求让大家以最轻松的姿态理解吴恩达的视频,如有不妥的地方欢迎大家指正。


    本文所使用的资料已上传到百度网盘**【点击下载】**,提取码:qifu,请在开始之前下载好所需资料,或者在本文底部copy资料代码。


    【博主使用的python版本:3.6.2】


    开始之前

       在开始之前,我们简单说一下我们要做什么。我们要建立一个神经网络,它有一个隐藏层。你会发现这个模型和上一个逻辑回归实现的模型有很大的区别。你可以跟随我的步骤在Jupyter Notebook中一步步地把代码填进去,也可以直接复制完整代码,在完整代码在本文底部,testCases.py和planar_utils.py的完整代码也在最底部。在这篇文章中,我们会讲到以下的知识:

    • 构建具有单隐藏层的2类分类神经网络。
    • 使用具有非线性激活功能激活函数,例如tanh。
    • 计算交叉熵损失(损失函数)。
    • 实现向前和向后传播。

    准备软件包

    我们需要准备一些软件包:

    • numpy:是用Python进行科学计算的基本软件包。
    • sklearn:为数据挖掘和数据分析提供的简单高效的工具。
    • matplotlib :是一个用于在Python中绘制图表的库。
    • testCases:提供了一些测试示例来评估函数的正确性,参见下载的资料或者在底部查看它的代码。
    • planar_utils :提供了在这个任务中使用的各种有用的功能,参见下载的资料或者在底部查看它的代码。
    import numpy as np
    import matplotlib.pyplot as plt
    from testCases import *
    import sklearn
    import sklearn.datasets
    import sklearn.linear_model
    from planar_utils import plot_decision_boundary, sigmoid, load_planar_dataset, load_extra_datasets
    
    #%matplotlib inline #如果你使用用的是Jupyter Notebook的话请取消注释。
    
    np.random.seed(1) #设置一个固定的随机种子,以保证接下来的步骤中我们的结果是一致的。
    
    

    加载和查看数据集

    首先,我们来看看我们将要使用的数据集, 下面的代码会将一个花的图案的2类数据集加载到变量X和Y中。

    X, Y = load_planar_dataset()
    

      把数据集加载完成了,然后使用matplotlib可视化数据集,代码如下:

    plt.scatter(X[0, :], X[1, :], c=Y, s=40, cmap=plt.cm.Spectral) #绘制散点图
    
    # 上一语句如出现问题,请使用下面的语句:
    plt.scatter(X[0, :], X[1, :], c=np.squeeze(Y), s=40, cmap=plt.cm.Spectral) #绘制散点图
    

    flower
       数据看起来像一朵红色(y = 0)和一些蓝色(y = 1)的数据点的花朵的图案。 我们的目标是建立一个模型来适应这些数据。现在,我们已经有了以下的东西:

    • X:一个numpy的矩阵,包含了这些数据点的数值
    • Y:一个numpy的向量,对应着的是X的标签【0 | 1】(红色:0 , 蓝色 :1)

    我们继续来仔细地看数据:

    shape_X = X.shape
    shape_Y = Y.shape
    m = Y.shape[1]  # 训练集里面的数量
    
    print ("X的维度为: " + str(shape_X))
    print ("Y的维度为: " + str(shape_Y))
    print ("数据集里面的数据有:" + str(m) + " 个")
    

    运行结果为:

    X的维度为: (2, 400)
    Y的维度为: (1, 400)
    数据集里面的数据有:400

    查看简单的Logistic回归的分类效果

      在构建完整的神经网络之前,先让我们看看逻辑回归在这个问题上的表现如何,我们可以使用sklearn的内置函数来做到这一点, 运行下面的代码来训练数据集上的逻辑回归分类器。

    clf = sklearn.linear_model.LogisticRegressionCV()
    clf.fit(X.T,Y.T)
    

      这里会打印出以下的信息(不同的机器提示大同小异):
      E:\Anaconda3\lib\site-packages\sklearn\utils\validation.py:547: DataConversionWarning: A column-vector y was passed when a 1d array was expected. Please change the shape of y to (n_samples, ), for example using ravel().
      y = column_or_1d(y, warn=True)

    我们可以把逻辑回归分类器的分类绘制出来:

    plot_decision_boundary(lambda x: clf.predict(x), X, Y) #绘制决策边界
    plt.title("Logistic Regression") #图标题
    LR_predictions  = clf.predict(X.T) #预测结果
    print ("逻辑回归的准确性: %d " % float((np.dot(Y, LR_predictions) + 
    		np.dot(1 - Y,1 - LR_predictions)) / float(Y.size) * 100) +
           "% " + "(正确标记的数据点所占的百分比)")
    

    我们看一看都打印了些什么吧!

    逻辑回归的准确性: 47 % (正确标记的数据点所占的百分比)
    

    Logistic Regression
    准确性只有47%的原因是数据集不是线性可分的,所以逻辑回归表现不佳,现在我们正式开始构建神经网络。


    搭建神经网络

    我们要搭建的神经网络模型如下图:
    Neural Network model image
    当然还有我们的理论基础(不懂可以去仔细看看视频):
    对于x(i)x^{(i)} 而言:
    z[1](i)=W[1]x(i)+b[1](i)(1)z^{[1] (i)} = W^{[1]} x^{(i)} + b^{[1] (i)}\tag{1}
    a[1](i)=tanh(z[1](i))(2)a^{[1] (i)} = \tanh(z^{[1] (i)})\tag{2}
    z[2](i)=W[2]a[1](i)+b[2](i)(3)z^{[2] (i)} = W^{[2]} a^{[1] (i)} + b^{[2] (i)}\tag{3}
    y^(i)=a[2](i)=σ(z[2](i))(4)\hat{y}^{(i)} = a^{[2] (i)} = \sigma(z^{ [2] (i)})\tag{4}
    KaTeX parse error: Undefined control sequence: \mbox at position 42: …gin{cases} 1 & \̲m̲b̲o̲x̲{if } a^{[2](i)…
    给出所有示例的预测结果,可以按如下方式计算成本J:
    J=1mi=0m(y(i)log(a[2](i))+(1y(i))log(1a[2](i)))(6)J = - \frac{1}{m} \sum\limits_{i = 0}^{m} \large\left(\small y^{(i)}\log\left(a^{[2] (i)}\right) + (1-y^{(i)})\log\left(1- a^{[2] (i)}\right) \large \right) \small \tag{6}

    构建神经网络的一般方法是:

    1. 定义神经网络结构(输入单元的数量,隐藏单元的数量等)。
    2. 初始化模型的参数
    3. 循环
    • 实施前向传播
    • 计算损失
    • 实现向后传播
    • 更新参数(梯度下降)

      我们要它们合并到一个nn_model() 函数中,当我们构建好了nn_model()并学习了正确的参数,我们就可以预测新的数据。

    定义神经网络结构

    在构建之前,我们要先把神经网络的结构给定义好:

    • n_x: 输入层的数量
    • n_h: 隐藏层的数量(这里设置为4)
    • n_y: 输出层的数量
    def layer_sizes(X , Y):
        """
        参数:
         X - 输入数据集,维度为(输入的数量,训练/测试的数量)
         Y - 标签,维度为(输出的数量,训练/测试数量)
        
        返回:
         n_x - 输入层的数量
         n_h - 隐藏层的数量
         n_y - 输出层的数量
        """
        n_x = X.shape[0] #输入层
        n_h = 4 #,隐藏层,硬编码为4
        n_y = Y.shape[0] #输出层
        
        return (n_x,n_h,n_y)
    

    我们来测试一下:

    #测试layer_sizes
    print("=========================测试layer_sizes=========================")
    X_asses , Y_asses = layer_sizes_test_case()
    (n_x,n_h,n_y) =  layer_sizes(X_asses,Y_asses)
    print("输入层的节点数量为: n_x = " + str(n_x))
    print("隐藏层的节点数量为: n_h = " + str(n_h))
    print("输出层的节点数量为: n_y = " + str(n_y))
    

    运行结果如下:

    =========================测试layer_sizes=========================
    输入层的节点数量为: n_x = 5
    隐藏层的节点数量为: n_h = 4
    输出层的节点数量为: n_y = 2
    

    初始化模型的参数

    在这里,我们要实现函数initialize_parameters()。我们要确保我们的参数大小合适,如果需要的话,请参考上面的神经网络图。
    我们将会用随机值初始化权重矩阵。

    • np.random.randn(a,b)* 0.01来随机初始化一个维度为(a,b)的矩阵。

    将偏向量初始化为零。

    • np.zeros((a,b))用零初始化矩阵(a,b)。
    def initialize_parameters( n_x , n_h ,n_y):
        """
        参数:
            n_x - 输入层节点的数量
            n_h - 隐藏层节点的数量
            n_y - 输出层节点的数量
        
        返回:
            parameters - 包含参数的字典:
                W1 - 权重矩阵,维度为(n_h,n_x)
                b1 - 偏向量,维度为(n_h,1)
                W2 - 权重矩阵,维度为(n_y,n_h)
                b2 - 偏向量,维度为(n_y,1)
    
        """
        np.random.seed(2) #指定一个随机种子,以便你的输出与我们的一样。
        W1 = np.random.randn(n_h,n_x) * 0.01
        b1 = np.zeros(shape=(n_h, 1))
        W2 = np.random.randn(n_y,n_h) * 0.01
        b2 = np.zeros(shape=(n_y, 1))
        
        #使用断言确保我的数据格式是正确的
        assert(W1.shape == ( n_h , n_x ))
        assert(b1.shape == ( n_h , 1 ))
        assert(W2.shape == ( n_y , n_h ))
        assert(b2.shape == ( n_y , 1 ))
        
        parameters = {"W1" : W1,
    	              "b1" : b1,
    	              "W2" : W2,
    	              "b2" : b2 }
        
        return parameters
    

    测试一下我们的代码:

    #测试initialize_parameters
    print("=========================测试initialize_parameters=========================")    
    n_x , n_h , n_y = initialize_parameters_test_case()
    parameters = initialize_parameters(n_x , n_h , n_y)
    print("W1 = " + str(parameters["W1"]))
    print("b1 = " + str(parameters["b1"]))
    print("W2 = " + str(parameters["W2"]))
    print("b2 = " + str(parameters["b2"]))
    
    

    结果如下:

    =========================测试initialize_parameters=========================
    W1 = [[-0.00416758 -0.00056267]
     [-0.02136196  0.01640271]
     [-0.01793436 -0.00841747]
     [ 0.00502881 -0.01245288]]
    b1 = [[ 0.]
     [ 0.]
     [ 0.]
     [ 0.]]
    W2 = [[-0.01057952 -0.00909008  0.00551454  0.02292208]]
    b2 = [[ 0.]]
    

    循环

    前向传播

    我们现在要实现前向传播函数forward_propagation()。
    我们可以使用sigmoid()函数,也可以使用np.tanh()函数。
    步骤如下:

    • 使用字典类型的parameters(它是initialize_parameters() 的输出)检索每个参数。
    • 实现向前传播, 计算Z[1],A[1],Z[2]Z^{[1]}, A^{[1]}, Z^{[2]}A[2]A^{[2]}( 训练集里面所有例子的预测向量)。
    • 反向传播所需的值存储在“cache”中,cache将作为反向传播函数的输入。
    def forward_propagation( X , parameters ):
        """
        参数:
             X - 维度为(n_x,m)的输入数据。
             parameters - 初始化函数(initialize_parameters)的输出
        
        返回:
             A2 - 使用sigmoid()函数计算的第二次激活后的数值
             cache - 包含“Z1”,“A1”,“Z2”和“A2”的字典类型变量
         """
        W1 = parameters["W1"]
        b1 = parameters["b1"]
        W2 = parameters["W2"]
        b2 = parameters["b2"]
        #前向传播计算A2
        Z1 = np.dot(W1 , X) + b1
        A1 = np.tanh(Z1)
        Z2 = np.dot(W2 , A1) + b2
        A2 = sigmoid(Z2)
        #使用断言确保我的数据格式是正确的
        assert(A2.shape == (1,X.shape[1]))
        cache = {"Z1": Z1,
                 "A1": A1,
                 "Z2": Z2,
                 "A2": A2}
        
        return (A2, cache)
    

    测试一下我的这个功能:

    #测试forward_propagation
    print("=========================测试forward_propagation=========================") 
    X_assess, parameters = forward_propagation_test_case()
    A2, cache = forward_propagation(X_assess, parameters)
    print(np.mean(cache["Z1"]), np.mean(cache["A1"]), np.mean(cache["Z2"]), np.mean(cache["A2"]))
    

    测试结果如下:

    =========================测试forward_propagation=========================
    -0.000499755777742 -0.000496963353232 0.000438187450959 0.500109546852
    

    现在我们已经计算了A[2]A^{[2]}a[2](i)a^{[2](i)}包含了训练集里每个数值,现在我们就可以构建成本函数了。

    计算损失

    计算成本的公式如下:
    J=1mi=0m(y(i)log(a[2](i))+(1y(i))log(1a[2](i)))(6)J = - \frac{1}{m} \sum\limits_{i = 0}^{m} \large\left(\small y^{(i)}\log\left(a^{[2] (i)}\right) + (1-y^{(i)})\log\left(1- a^{[2] (i)}\right) \large \right) \small \tag{6}
    有很多的方法都可以计算交叉熵损失,比如下面的这个公式,我们在python中可以这么实现:
    KaTeX parse error: \tag works only in display equations:

    logprobs = np.multiply(np.log(A2),Y)
    cost = - np.sum(logprobs)                # 不需要使用循环就可以直接算出来。
    

    当然,你也可以使用np.multiply()然后使用np.sum()或者直接使用np.dot()
    现在我们正式开始构建计算成本的函数:

    def compute_cost(A2,Y,parameters):
        """
        计算方程(6)中给出的交叉熵成本,
        
        参数:
             A2 - 使用sigmoid()函数计算的第二次激活后的数值
             Y - "True"标签向量,维度为(1,数量)
             parameters - 一个包含W1,B1,W2和B2的字典类型的变量
        
        返回:
             成本 - 交叉熵成本给出方程(13)
        """
        
        m = Y.shape[1]
        W1 = parameters["W1"]
        W2 = parameters["W2"]
        
        #计算成本
        logprobs = logprobs = np.multiply(np.log(A2), Y) + np.multiply((1 - Y), np.log(1 - A2))
        cost = - np.sum(logprobs) / m
        cost = float(np.squeeze(cost))
        
        assert(isinstance(cost,float))
        
        return cost
    

    测试一下我们的成本函数:

    #测试compute_cost
    print("=========================测试compute_cost=========================") 
    A2 , Y_assess , parameters = compute_cost_test_case()
    print("cost = " + str(compute_cost(A2,Y_assess,parameters)))
    

    测试结果如下:

    =========================测试compute_cost=========================
    cost = 0.6929198937761266
    

    使用正向传播期间计算的cache,现在可以利用它实现反向传播。

    现在我们要开始实现函数backward_propagation()。

    向后传播

      说明:反向传播通常是深度学习中最难(数学意义)部分,为了帮助你,这里有反向传播讲座的幻灯片, 由于我们正在构建向量化实现,因此我们将需要使用这下面的六个方程:
    Summary of gradient desent
    为了计算dZ1,里需要计算 g[1](Z[1])g^{[1]'}(Z^{[1]})g[1](...)g^{[1]}(...) 是tanh激活函数,如果a=g[1](z)a = g^{[1]}(z) 那么g[1](z)=1a2g^{[1]'}(z) = 1-a^2。所以我们需要使用 (1 - np.power(A1, 2))来计算g[1](Z[1])g^{[1]'}(Z^{[1]})

    def backward_propagation(parameters,cache,X,Y):
        """
        使用上述说明搭建反向传播函数。
        
        参数:
         parameters - 包含我们的参数的一个字典类型的变量。
         cache - 包含“Z1”,“A1”,“Z2”和“A2”的字典类型的变量。
         X - 输入数据,维度为(2,数量)
         Y - “True”标签,维度为(1,数量)
        
        返回:
         grads - 包含W和b的导数一个字典类型的变量。
        """
        m = X.shape[1]
        
        W1 = parameters["W1"]
        W2 = parameters["W2"]
        
        A1 = cache["A1"]
        A2 = cache["A2"]
        
        dZ2= A2 - Y
        dW2 = (1 / m) * np.dot(dZ2, A1.T)
        db2 = (1 / m) * np.sum(dZ2, axis=1, keepdims=True)
        dZ1 = np.multiply(np.dot(W2.T, dZ2), 1 - np.power(A1, 2))
        dW1 = (1 / m) * np.dot(dZ1, X.T)
        db1 = (1 / m) * np.sum(dZ1, axis=1, keepdims=True)
        grads = {"dW1": dW1,
                 "db1": db1,
                 "dW2": dW2,
                 "db2": db2 }
        
        return grads
    

    测试一下反向传播函数:

    #测试backward_propagation
    print("=========================测试backward_propagation=========================")
    parameters, cache, X_assess, Y_assess = backward_propagation_test_case()
    
    grads = backward_propagation(parameters, cache, X_assess, Y_assess)
    print ("dW1 = "+ str(grads["dW1"]))
    print ("db1 = "+ str(grads["db1"]))
    print ("dW2 = "+ str(grads["dW2"]))
    print ("db2 = "+ str(grads["db2"]))
    

    测试结果如下:

    =========================测试backward_propagation=========================
    dW1 = [[ 0.01018708 -0.00708701]
     [ 0.00873447 -0.0060768 ]
     [-0.00530847  0.00369379]
     [-0.02206365  0.01535126]]
    db1 = [[-0.00069728]
     [-0.00060606]
     [ 0.000364  ]
     [ 0.00151207]]
    dW2 = [[ 0.00363613  0.03153604  0.01162914 -0.01318316]]
    db2 = [[ 0.06589489]]
    

    反向传播完成了,我们开始对参数进行更新

    更新参数

    我们需要使用(dW1, db1, dW2, db2)来更新(W1, b1, W2, b2)。
    更新算法如下:
    $ \theta = \theta - \alpha \frac{\partial J }{ \partial \theta }$

    • α\alpha:学习速率
    • θ\theta :参数

    我们需要选择一个良好的学习速率,我们可以看一下下面这两个图(由Adam Harley提供):
    sgdsgd_bad
    上面两个图分别代表了具有良好学习速率(收敛)和不良学习速率(发散)的梯度下降算法。

    def update_parameters(parameters,grads,learning_rate=1.2):
        """
        使用上面给出的梯度下降更新规则更新参数
        
        参数:
         parameters - 包含参数的字典类型的变量。
         grads - 包含导数值的字典类型的变量。
         learning_rate - 学习速率
        
        返回:
         parameters - 包含更新参数的字典类型的变量。
        """
        W1,W2 = parameters["W1"],parameters["W2"]
        b1,b2 = parameters["b1"],parameters["b2"]
        
        dW1,dW2 = grads["dW1"],grads["dW2"]
        db1,db2 = grads["db1"],grads["db2"]
        
        W1 = W1 - learning_rate * dW1
        b1 = b1 - learning_rate * db1
        W2 = W2 - learning_rate * dW2
        b2 = b2 - learning_rate * db2
        
        parameters = {"W1": W1,
                      "b1": b1,
                      "W2": W2,
                      "b2": b2}
        
        return parameters
    

    测试一下update_parameters():

    #测试update_parameters
    print("=========================测试update_parameters=========================")
    parameters, grads = update_parameters_test_case()
    parameters = update_parameters(parameters, grads)
    
    print("W1 = " + str(parameters["W1"]))
    print("b1 = " + str(parameters["b1"]))
    print("W2 = " + str(parameters["W2"]))
    print("b2 = " + str(parameters["b2"]))
    

    测试结果如下:

    =========================测试update_parameters=========================
    W1 = [[-0.00643025  0.01936718]
     [-0.02410458  0.03978052]
     [-0.01653973 -0.02096177]
     [ 0.01046864 -0.05990141]]
    b1 = [[ -1.02420756e-06]
     [  1.27373948e-05]
     [  8.32996807e-07]
     [ -3.20136836e-06]]
    W2 = [[-0.01041081 -0.04463285  0.01758031  0.04747113]]
    b2 = [[ 0.00010457]]
    

    整合

    我们现在把上面的东西整合到nn_model()中,神经网络模型必须以正确的顺序使用先前的功能。

    def nn_model(X,Y,n_h,num_iterations,print_cost=False):
        """
        参数:
            X - 数据集,维度为(2,示例数)
            Y - 标签,维度为(1,示例数)
            n_h - 隐藏层的数量
            num_iterations - 梯度下降循环中的迭代次数
            print_cost - 如果为True,则每1000次迭代打印一次成本数值
        
        返回:
            parameters - 模型学习的参数,它们可以用来进行预测。
         """
         
        np.random.seed(3) #指定随机种子
        n_x = layer_sizes(X, Y)[0]
        n_y = layer_sizes(X, Y)[2]
        
        parameters = initialize_parameters(n_x,n_h,n_y)
        W1 = parameters["W1"]
        b1 = parameters["b1"]
        W2 = parameters["W2"]
        b2 = parameters["b2"]
        
        for i in range(num_iterations):
            A2 , cache = forward_propagation(X,parameters)
            cost = compute_cost(A2,Y,parameters)
            grads = backward_propagation(parameters,cache,X,Y)
            parameters = update_parameters(parameters,grads,learning_rate = 0.5)
            
            if print_cost:
                if i%1000 == 0:
                    print("第 ",i," 次循环,成本为:"+str(cost))
        return parameters
    

    测试nn_model():

    #测试nn_model
    print("=========================测试nn_model=========================")
    X_assess, Y_assess = nn_model_test_case()
    
    parameters = nn_model(X_assess, Y_assess, 4, num_iterations=10000, print_cost=False)
    print("W1 = " + str(parameters["W1"]))
    print("b1 = " + str(parameters["b1"]))
    print("W2 = " + str(parameters["W2"]))
    print("b2 = " + str(parameters["b2"]))
    

    测试结果如下:

    =========================测试nn_model=========================
    W1 = [[-4.18494482  5.33220319]
     [-7.52989354  1.24306197]
     [-4.19295428  5.32631786]
     [ 7.52983748 -1.24309404]]
    b1 = [[ 2.32926815]
     [ 3.7945905 ]
     [ 2.33002544]
     [-3.79468791]]
    W2 = [[-6033.83672179 -6008.12981272 -6033.10095329  6008.06636901]]
    b2 = [[-52.66607704]]
    

    参数更新完了我们就可以来进行预测了。

    预测

    构建predict()来使用模型进行预测, 使用向前传播来预测结果。
    predictions = KaTeX parse error: Undefined control sequence: \ at position 46: …1 & 激活值> 0.5 \\\̲ ̲ 0 & \text…

    def predict(parameters,X):
        """
        使用学习的参数,为X中的每个示例预测一个类
        
        参数:
    		parameters - 包含参数的字典类型的变量。
    	    X - 输入数据(n_x,m)
        
        返回
    		predictions - 我们模型预测的向量(红色:0 /蓝色:1)
         
         """
        A2 , cache = forward_propagation(X,parameters)
        predictions = np.round(A2)
        
        return predictions
    

    测试一下predict

    #测试predict
    print("=========================测试predict=========================")
    
    parameters, X_assess = predict_test_case()
    
    predictions = predict(parameters, X_assess)
    print("预测的平均值 = " + str(np.mean(predictions)))
    

    测试结果:

    =========================测试predict=========================
    预测的平均值 = 0.666666666667
    

    现在我们把所有的东西基本都做完了,我们开始正式运行。


    正式运行

    parameters = nn_model(X, Y, n_h = 4, num_iterations=10000, print_cost=True)
    
    #绘制边界
    plot_decision_boundary(lambda x: predict(parameters, x.T), X, Y)
    plt.title("Decision Boundary for hidden layer size " + str(4))
    
    predictions = predict(parameters, X)
    print ('准确率: %d' % float((np.dot(Y, predictions.T) + np.dot(1 - Y, 1 - predictions.T)) / float(Y.size) * 100) + '%')
    

    运行结果:

    0  次循环,成本为:0.69304802012398231000  次循环,成本为:0.288083293569018352000  次循环,成本为:0.254385494073244963000  次循环,成本为:0.233864150389521964000  次循环,成本为:0.226792487448540085000  次循环,成本为:0.222644275492990156000  次循环,成本为:0.219731404042813167000  次循环,成本为:0.217503654051312948000  次循环,成本为:0.219503964694673159000  次循环,成本为:0.2185709575018246
    准确率: 90%
    

    resulat


    更改隐藏层节点数量

    我们上面的实验把隐藏层定为4个节点,现在我们更改隐藏层里面的节点数量,看一看节点数量是否会对结果造成影响。

    plt.figure(figsize=(16, 32))
    hidden_layer_sizes = [1, 2, 3, 4, 5, 20, 50] #隐藏层数量
    for i, n_h in enumerate(hidden_layer_sizes):
        plt.subplot(5, 2, i + 1)
        plt.title('Hidden Layer of size %d' % n_h)
        parameters = nn_model(X, Y, n_h, num_iterations=5000)
        plot_decision_boundary(lambda x: predict(parameters, x.T), X, Y)
        predictions = predict(parameters, X)
        accuracy = float((np.dot(Y, predictions.T) + np.dot(1 - Y, 1 - predictions.T)) / float(Y.size) * 100)
        print ("隐藏层的节点数量: {}  ,准确率: {} %".format(n_h, accuracy))
    

    打印结果:

    隐藏层的节点数量: 1  ,准确率: 67.5 %
    隐藏层的节点数量: 2  ,准确率: 67.25 %
    隐藏层的节点数量: 3  ,准确率: 90.75 %
    隐藏层的节点数量: 4  ,准确率: 90.5 %
    隐藏层的节点数量: 5  ,准确率: 91.25 %
    隐藏层的节点数量: 20  ,准确率: 90.0 %
    隐藏层的节点数量: 50  ,准确率: 90.75 %
    

    units

    较大的模型(具有更多隐藏单元)能够更好地适应训练集,直到最终的最大模型过度拟合数据。
    最好的隐藏层大小似乎在n_h = 5附近。实际上,这里的值似乎很适合数据,而且不会引起过度拟合。
    我们还将在后面学习有关正则化的知识,它允许我们使用非常大的模型(如n_h = 50),而不会出现太多过度拟合。


    ##【可选】探索

    • 当改变sigmoid激活或ReLU激活的tanh激活时会发生什么?
    • 改变learning_rate的数值会发生什么
    • 如果我们改变数据集呢?
    # 数据集
    noisy_circles, noisy_moons, blobs, gaussian_quantiles, no_structure = load_extra_datasets()
    
    datasets = {"noisy_circles": noisy_circles,
                "noisy_moons": noisy_moons,
                "blobs": blobs,
                "gaussian_quantiles": gaussian_quantiles}
    
    dataset = "noisy_moons"
    
    X, Y = datasets[dataset]
    X, Y = X.T, Y.reshape(1, Y.shape[0])
    
    if dataset == "blobs":
        Y = Y % 2
    
    plt.scatter(X[0, :], X[1, :], c=Y, s=40, cmap=plt.cm.Spectral)
    
    #上一语句如出现问题请使用下面的语句:
    plt.scatter(X[0, :], X[1, :], c=np.squeeze(Y), s=40, cmap=plt.cm.Spectral)
    

    new dataset


    完整代码

    作业代码

    # -*- coding: utf-8 -*-
    """
    本文博客地址:https://blog.csdn.net/u013733326/article/details/79702148
    
    @author: Oscar
    """
    
    import numpy as np
    import matplotlib.pyplot as plt
    from testCases import *
    import sklearn
    import sklearn.datasets
    import sklearn.linear_model
    from planar_utils import plot_decision_boundary, sigmoid, load_planar_dataset, load_extra_datasets
    
    #%matplotlib inline #如果你使用用的是Jupyter Notebook的话请取消注释。
    
    np.random.seed(1) #设置一个固定的随机种子,以保证接下来的步骤中我们的结果是一致的。
    
    X, Y = load_planar_dataset()
    #plt.scatter(X[0, :], X[1, :], c=Y, s=40, cmap=plt.cm.Spectral) #绘制散点图
    shape_X = X.shape
    shape_Y = Y.shape
    m = Y.shape[1]  # 训练集里面的数量
    
    print ("X的维度为: " + str(shape_X))
    print ("Y的维度为: " + str(shape_Y))
    print ("数据集里面的数据有:" + str(m) + " 个")
    
    def layer_sizes(X , Y):
        """
        参数:
         X - 输入数据集,维度为(输入的数量,训练/测试的数量)
         Y - 标签,维度为(输出的数量,训练/测试数量)
    
        返回:
         n_x - 输入层的数量
         n_h - 隐藏层的数量
         n_y - 输出层的数量
        """
        n_x = X.shape[0] #输入层
        n_h = 4 #,隐藏层,硬编码为4
        n_y = Y.shape[0] #输出层
    
        return (n_x,n_h,n_y)
    
    def initialize_parameters( n_x , n_h ,n_y):
        """
        参数:
            n_x - 输入节点的数量
            n_h - 隐藏层节点的数量
            n_y - 输出层节点的数量
    
        返回:
            parameters - 包含参数的字典:
                W1 - 权重矩阵,维度为(n_h,n_x)
                b1 - 偏向量,维度为(n_h,1)
                W2 - 权重矩阵,维度为(n_y,n_h)
                b2 - 偏向量,维度为(n_y,1)
    
        """
        np.random.seed(2) #指定一个随机种子,以便你的输出与我们的一样。
        W1 = np.random.randn(n_h,n_x) * 0.01
        b1 = np.zeros(shape=(n_h, 1))
        W2 = np.random.randn(n_y,n_h) * 0.01
        b2 = np.zeros(shape=(n_y, 1))
    
        #使用断言确保我的数据格式是正确的
        assert(W1.shape == ( n_h , n_x ))
        assert(b1.shape == ( n_h , 1 ))
        assert(W2.shape == ( n_y , n_h ))
        assert(b2.shape == ( n_y , 1 ))
    
        parameters = {"W1" : W1,
                      "b1" : b1,
                      "W2" : W2,
                      "b2" : b2 }
    
        return parameters
    
    def forward_propagation( X , parameters ):
        """
        参数:
             X - 维度为(n_x,m)的输入数据。
             parameters - 初始化函数(initialize_parameters)的输出
    
        返回:
             A2 - 使用sigmoid()函数计算的第二次激活后的数值
             cache - 包含“Z1”,“A1”,“Z2”和“A2”的字典类型变量
         """
        W1 = parameters["W1"]
        b1 = parameters["b1"]
        W2 = parameters["W2"]
        b2 = parameters["b2"]
        #前向传播计算A2
        Z1 = np.dot(W1 , X) + b1
        A1 = np.tanh(Z1)
        Z2 = np.dot(W2 , A1) + b2
        A2 = sigmoid(Z2)
        #使用断言确保我的数据格式是正确的
        assert(A2.shape == (1,X.shape[1]))
        cache = {"Z1": Z1,
                 "A1": A1,
                 "Z2": Z2,
                 "A2": A2}
    
        return (A2, cache)
    
    def compute_cost(A2,Y,parameters):
        """
        计算方程(6)中给出的交叉熵成本,
    
        参数:
             A2 - 使用sigmoid()函数计算的第二次激活后的数值
             Y - "True"标签向量,维度为(1,数量)
             parameters - 一个包含W1,B1,W2和B2的字典类型的变量
    
        返回:
             成本 - 交叉熵成本给出方程(13)
        """
    
        m = Y.shape[1]
        W1 = parameters["W1"]
        W2 = parameters["W2"]
    
        #计算成本
        logprobs = logprobs = np.multiply(np.log(A2), Y) + np.multiply((1 - Y), np.log(1 - A2))
        cost = - np.sum(logprobs) / m
        cost = float(np.squeeze(cost))
    
        assert(isinstance(cost,float))
    
        return cost
    
    def backward_propagation(parameters,cache,X,Y):
        """
        使用上述说明搭建反向传播函数。
    
        参数:
         parameters - 包含我们的参数的一个字典类型的变量。
         cache - 包含“Z1”,“A1”,“Z2”和“A2”的字典类型的变量。
         X - 输入数据,维度为(2,数量)
         Y - “True”标签,维度为(1,数量)
    
        返回:
         grads - 包含W和b的导数一个字典类型的变量。
        """
        m = X.shape[1]
    
        W1 = parameters["W1"]
        W2 = parameters["W2"]
    
        A1 = cache["A1"]
        A2 = cache["A2"]
    
        dZ2= A2 - Y
        dW2 = (1 / m) * np.dot(dZ2, A1.T)
        db2 = (1 / m) * np.sum(dZ2, axis=1, keepdims=True)
        dZ1 = np.multiply(np.dot(W2.T, dZ2), 1 - np.power(A1, 2))
        dW1 = (1 / m) * np.dot(dZ1, X.T)
        db1 = (1 / m) * np.sum(dZ1, axis=1, keepdims=True)
        grads = {"dW1": dW1,
                 "db1": db1,
                 "dW2": dW2,
                 "db2": db2 }
    
        return grads
    
    def update_parameters(parameters,grads,learning_rate=1.2):
        """
        使用上面给出的梯度下降更新规则更新参数
    
        参数:
         parameters - 包含参数的字典类型的变量。
         grads - 包含导数值的字典类型的变量。
         learning_rate - 学习速率
    
        返回:
         parameters - 包含更新参数的字典类型的变量。
        """
        W1,W2 = parameters["W1"],parameters["W2"]
        b1,b2 = parameters["b1"],parameters["b2"]
    
        dW1,dW2 = grads["dW1"],grads["dW2"]
        db1,db2 = grads["db1"],grads["db2"]
    
        W1 = W1 - learning_rate * dW1
        b1 = b1 - learning_rate * db1
        W2 = W2 - learning_rate * dW2
        b2 = b2 - learning_rate * db2
    
        parameters = {"W1": W1,
                      "b1": b1,
                      "W2": W2,
                      "b2": b2}
    
        return parameters
    
    def nn_model(X,Y,n_h,num_iterations,print_cost=False):
        """
        参数:
            X - 数据集,维度为(2,示例数)
            Y - 标签,维度为(1,示例数)
            n_h - 隐藏层的数量
            num_iterations - 梯度下降循环中的迭代次数
            print_cost - 如果为True,则每1000次迭代打印一次成本数值
    
        返回:
            parameters - 模型学习的参数,它们可以用来进行预测。
         """
    
        np.random.seed(3) #指定随机种子
        n_x = layer_sizes(X, Y)[0]
        n_y = layer_sizes(X, Y)[2]
    
        parameters = initialize_parameters(n_x,n_h,n_y)
        W1 = parameters["W1"]
        b1 = parameters["b1"]
        W2 = parameters["W2"]
        b2 = parameters["b2"]
    
        for i in range(num_iterations):
            A2 , cache = forward_propagation(X,parameters)
            cost = compute_cost(A2,Y,parameters)
            grads = backward_propagation(parameters,cache,X,Y)
            parameters = update_parameters(parameters,grads,learning_rate = 0.5)
    
            if print_cost:
                if i%1000 == 0:
                    print("第 ",i," 次循环,成本为:"+str(cost))
        return parameters
    
    def predict(parameters,X):
        """
        使用学习的参数,为X中的每个示例预测一个类
    
        参数:
            parameters - 包含参数的字典类型的变量。
            X - 输入数据(n_x,m)
    
        返回
            predictions - 我们模型预测的向量(红色:0 /蓝色:1)
    
         """
        A2 , cache = forward_propagation(X,parameters)
        predictions = np.round(A2)
    
        return predictions
    
    parameters = nn_model(X, Y, n_h = 4, num_iterations=10000, print_cost=True)
    
    #绘制边界
    plot_decision_boundary(lambda x: predict(parameters, x.T), X, Y)
    plt.title("Decision Boundary for hidden layer size " + str(4))
    
    predictions = predict(parameters, X)
    print ('准确率: %d' % float((np.dot(Y, predictions.T) + np.dot(1 - Y, 1 - predictions.T)) / float(Y.size) * 100) + '%')
    
    """
    plt.figure(figsize=(16, 32))
    hidden_layer_sizes = [1, 2, 3, 4, 5, 20, 50] #隐藏层数量
    for i, n_h in enumerate(hidden_layer_sizes):
        plt.subplot(5, 2, i + 1)
        plt.title('Hidden Layer of size %d' % n_h)
        parameters = nn_model(X, Y, n_h, num_iterations=5000)
        plot_decision_boundary(lambda x: predict(parameters, x.T), X, Y)
        predictions = predict(parameters, X)
        accuracy = float((np.dot(Y, predictions.T) + np.dot(1 - Y, 1 - predictions.T)) / float(Y.size) * 100)
        print ("隐藏层的节点数量: {}  ,准确率: {} %".format(n_h, accuracy))
    """
    

    testCases.py

    #-*- coding: UTF-8 -*-
    """
    # WANGZHE12
    """
    import numpy as np
    
    def layer_sizes_test_case():
        np.random.seed(1)
        X_assess = np.random.randn(5, 3)
        Y_assess = np.random.randn(2, 3)
        return X_assess, Y_assess
    
    def initialize_parameters_test_case():
        n_x, n_h, n_y = 2, 4, 1
        return n_x, n_h, n_y
    
    def forward_propagation_test_case():
        np.random.seed(1)
        X_assess = np.random.randn(2, 3)
    
        parameters = {'W1': np.array([[-0.00416758, -0.00056267],
            [-0.02136196,  0.01640271],
            [-0.01793436, -0.00841747],
            [ 0.00502881, -0.01245288]]),
         'W2': np.array([[-0.01057952, -0.00909008,  0.00551454,  0.02292208]]),
         'b1': np.array([[ 0.],
            [ 0.],
            [ 0.],
            [ 0.]]),
         'b2': np.array([[ 0.]])}
    
        return X_assess, parameters
    
    def compute_cost_test_case():
        np.random.seed(1)
        Y_assess = np.random.randn(1, 3)
        parameters = {'W1': np.array([[-0.00416758, -0.00056267],
            [-0.02136196,  0.01640271],
            [-0.01793436, -0.00841747],
            [ 0.00502881, -0.01245288]]),
         'W2': np.array([[-0.01057952, -0.00909008,  0.00551454,  0.02292208]]),
         'b1': np.array([[ 0.],
            [ 0.],
            [ 0.],
            [ 0.]]),
         'b2': np.array([[ 0.]])}
    
        a2 = (np.array([[ 0.5002307 ,  0.49985831,  0.50023963]]))
    
        return a2, Y_assess, parameters
    
    def backward_propagation_test_case():
        np.random.seed(1)
        X_assess = np.random.randn(2, 3)
        Y_assess = np.random.randn(1, 3)
        parameters = {'W1': np.array([[-0.00416758, -0.00056267],
            [-0.02136196,  0.01640271],
            [-0.01793436, -0.00841747],
            [ 0.00502881, -0.01245288]]),
         'W2': np.array([[-0.01057952, -0.00909008,  0.00551454,  0.02292208]]),
         'b1': np.array([[ 0.],
            [ 0.],
            [ 0.],
            [ 0.]]),
         'b2': np.array([[ 0.]])}
    
        cache = {'A1': np.array([[-0.00616578,  0.0020626 ,  0.00349619],
             [-0.05225116,  0.02725659, -0.02646251],
             [-0.02009721,  0.0036869 ,  0.02883756],
             [ 0.02152675, -0.01385234,  0.02599885]]),
      'A2': np.array([[ 0.5002307 ,  0.49985831,  0.50023963]]),
      'Z1': np.array([[-0.00616586,  0.0020626 ,  0.0034962 ],
             [-0.05229879,  0.02726335, -0.02646869],
             [-0.02009991,  0.00368692,  0.02884556],
             [ 0.02153007, -0.01385322,  0.02600471]]),
      'Z2': np.array([[ 0.00092281, -0.00056678,  0.00095853]])}
        return parameters, cache, X_assess, Y_assess
    
    def update_parameters_test_case():
        parameters = {'W1': np.array([[-0.00615039,  0.0169021 ],
            [-0.02311792,  0.03137121],
            [-0.0169217 , -0.01752545],
            [ 0.00935436, -0.05018221]]),
     'W2': np.array([[-0.0104319 , -0.04019007,  0.01607211,  0.04440255]]),
     'b1': np.array([[ -8.97523455e-07],
            [  8.15562092e-06],
            [  6.04810633e-07],
            [ -2.54560700e-06]]),
     'b2': np.array([[  9.14954378e-05]])}
    
        grads = {'dW1': np.array([[ 0.00023322, -0.00205423],
            [ 0.00082222, -0.00700776],
            [-0.00031831,  0.0028636 ],
            [-0.00092857,  0.00809933]]),
     'dW2': np.array([[ -1.75740039e-05,   3.70231337e-03,  -1.25683095e-03,
              -2.55715317e-03]]),
     'db1': np.array([[  1.05570087e-07],
            [ -3.81814487e-06],
            [ -1.90155145e-07],
            [  5.46467802e-07]]),
     'db2': np.array([[ -1.08923140e-05]])}
        return parameters, grads
    
    def nn_model_test_case():
        np.random.seed(1)
        X_assess = np.random.randn(2, 3)
        Y_assess = np.random.randn(1, 3)
        return X_assess, Y_assess
    
    def predict_test_case():
        np.random.seed(1)
        X_assess = np.random.randn(2, 3)
        parameters = {'W1': np.array([[-0.00615039,  0.0169021 ],
            [-0.02311792,  0.03137121],
            [-0.0169217 , -0.01752545],
            [ 0.00935436, -0.05018221]]),
         'W2': np.array([[-0.0104319 , -0.04019007,  0.01607211,  0.04440255]]),
         'b1': np.array([[ -8.97523455e-07],
            [  8.15562092e-06],
            [  6.04810633e-07],
            [ -2.54560700e-06]]),
         'b2': np.array([[  9.14954378e-05]])}
        return parameters, X_assess
    
    

    planar_utils.py

    import matplotlib.pyplot as plt
    import numpy as np
    import sklearn
    import sklearn.datasets
    import sklearn.linear_model
    
    def plot_decision_boundary(model, X, y):
        # Set min and max values and give it some padding
        x_min, x_max = X[0, :].min() - 1, X[0, :].max() + 1
        y_min, y_max = X[1, :].min() - 1, X[1, :].max() + 1
        h = 0.01
        # Generate a grid of points with distance h between them
        xx, yy = np.meshgrid(np.arange(x_min, x_max, h), np.arange(y_min, y_max, h))
        # Predict the function value for the whole grid
        Z = model(np.c_[xx.ravel(), yy.ravel()])
        Z = Z.reshape(xx.shape)
        # Plot the contour and training examples
        plt.contourf(xx, yy, Z, cmap=plt.cm.Spectral)
        plt.ylabel('x2')
        plt.xlabel('x1')
        plt.scatter(X[0, :], X[1, :], c=np.squeeze(y), cmap=plt.cm.Spectral)
    
    
    def sigmoid(x):
        s = 1/(1+np.exp(-x))
        return s
    
    def load_planar_dataset():
        np.random.seed(1)
        m = 400 # number of examples
        N = int(m/2) # number of points per class
        D = 2 # dimensionality
        X = np.zeros((m,D)) # data matrix where each row is a single example
        Y = np.zeros((m,1), dtype='uint8') # labels vector (0 for red, 1 for blue)
        a = 4 # maximum ray of the flower
    
        for j in range(2):
            ix = range(N*j,N*(j+1))
            t = np.linspace(j*3.12,(j+1)*3.12,N) + np.random.randn(N)*0.2 # theta
            r = a*np.sin(4*t) + np.random.randn(N)*0.2 # radius
            X[ix] = np.c_[r*np.sin(t), r*np.cos(t)]
            Y[ix] = j
    
        X = X.T
        Y = Y.T
    
        return X, Y
    
    def load_extra_datasets():  
        N = 200
        noisy_circles = sklearn.datasets.make_circles(n_samples=N, factor=.5, noise=.3)
        noisy_moons = sklearn.datasets.make_moons(n_samples=N, noise=.2)
        blobs = sklearn.datasets.make_blobs(n_samples=N, random_state=5, n_features=2, centers=6)
        gaussian_quantiles = sklearn.datasets.make_gaussian_quantiles(mean=None, cov=0.5, n_samples=N, n_features=2, n_classes=2, shuffle=True, random_state=None)
        no_structure = np.random.rand(N, 2), np.random.rand(N, 2)
    
        return noisy_circles, noisy_moons, blobs, gaussian_quantiles, no_structure
    
    展开全文
  • 吴恩达课后编程作业】01 - 神经网络和深度学习 - 第四周 - PA1&amp;2 - 一步步搭建多层神经网络以及应用 上一篇:【课程1 - 第四周测验】※※※※※ 【回到目录】※※※※※下一篇:【课程2 - 第一周...

    【吴恩达课后编程作业】01 - 神经网络和深度学习 - 第四周 - PA1&2 - 一步步搭建多层神经网络以及应用


    上一篇:【课程1 - 第四周测验】※※※※※ 【回到目录】※※※※※下一篇:【课程2 - 第一周测验】

    声明

      本文参考Kulbear【Building your Deep Neural Network - Step by Step】【Deep Neural Network - Application】,以及念师【8. 多层神经网络代码实战】,我基于以上的文章加以自己的理解发表这篇博客,力求让大家以最轻松的姿态理解吴恩达的视频,如有不妥的地方欢迎大家指正。


    本文所使用的资料已上传到百度网盘【点击下载】,提取码:xx1w,请在开始之前下载好所需资料,或者在本文底部copy资料代码。


    【博主使用的python版本:3.6.2】


    开始之前

      在正式开始之前,我们先来了解一下我们要做什么。在本次教程中,我们要构建两个神经网络,一个是构建两层的神经网络,一个是构建多层的神经网络,多层神经网络的层数可以自己定义。本次的教程的难度有所提升,但是我会力求深入简出。在这里,我们简单的讲一下难点,本文会提到**[LINEAR-> ACTIVATION]转发函数,比如我有一个多层的神经网络,结构是输入层->隐藏层->隐藏层->···->隐藏层->输出层**,在每一层中,我会首先计算Z = np.dot(W,A) + b,这叫做【linear_forward】,然后再计算A = relu(Z) 或者 A = sigmoid(Z),这叫做【linear_activation_forward】,合并起来就是这一层的计算方法,所以每一层的计算都有两个步骤,先是计算Z,再计算A,你也可以参照下图:
    GALBOL

    我们来说一下步骤:

    1. 初始化网络参数

    2. 前向传播

      2.1 计算一层的中线性求和的部分

      2.2 计算激活函数的部分(ReLU使用L-1次,Sigmod使用1次)

      2.3 结合线性求和与激活函数

    3. 计算误差

    4. 反向传播

      4.1 线性部分的反向传播公式

      4.2 激活函数部分的反向传播公式

      4.3 结合线性部分与激活函数的反向传播公式

    5. 更新参数

      请注意,对于每个前向函数,都有一个相应的后向函数。 这就是为什么在我们的转发模块的每一步都会在cache中存储一些值,cache的值对计算梯度很有用, 在反向传播模块中,我们将使用cache来计算梯度。 现在我们正式开始分别构建两层神经网络和多层神经网络。


    准备软件包

    在开始我们需要准备一些软件包:

    import numpy as np
    import h5py
    import matplotlib.pyplot as plt
    import testCases #参见资料包,或者在文章底部copy
    from dnn_utils import sigmoid, sigmoid_backward, relu, relu_backward #参见资料包
    import lr_utils #参见资料包,或者在文章底部copy
    

    软件包准备好了,我们开始构建初始化参数的函数。

    为了和我的数据匹配,你需要指定随机种子

    np.random.seed(1)
    

    初始化参数

    对于一个两层的神经网络结构而言,模型结构是线性->ReLU->线性->sigmod函数。

    初始化函数如下:

    def initialize_parameters(n_x,n_h,n_y):
        """
        此函数是为了初始化两层网络参数而使用的函数。
        参数:
            n_x - 输入层节点数量
            n_h - 隐藏层节点数量
            n_y - 输出层节点数量
        
        返回:
            parameters - 包含你的参数的python字典:
                W1 - 权重矩阵,维度为(n_h,n_x)
                b1 - 偏向量,维度为(n_h,1)
                W2 - 权重矩阵,维度为(n_y,n_h)
                b2 - 偏向量,维度为(n_y,1)
    
        """
        W1 = np.random.randn(n_h, n_x) * 0.01
        b1 = np.zeros((n_h, 1))
        W2 = np.random.randn(n_y, n_h) * 0.01
        b2 = np.zeros((n_y, 1))
        
        #使用断言确保我的数据格式是正确的
        assert(W1.shape == (n_h, n_x))
        assert(b1.shape == (n_h, 1))
        assert(W2.shape == (n_y, n_h))
        assert(b2.shape == (n_y, 1))
        
        parameters = {"W1": W1,
                      "b1": b1,
                      "W2": W2,
                      "b2": b2}
        
        return parameters  
    

    初始化完成我们来测试一下:

    print("==============测试initialize_parameters==============")
    parameters = initialize_parameters(3,2,1)
    print("W1 = " + str(parameters["W1"]))
    print("b1 = " + str(parameters["b1"]))
    print("W2 = " + str(parameters["W2"]))
    print("b2 = " + str(parameters["b2"]))
    

    测试结果:

    ==============测试initialize_parameters==============
    W1 = [[ 0.01624345 -0.00611756 -0.00528172]
     [-0.01072969  0.00865408 -0.02301539]]
    b1 = [[ 0.]
     [ 0.]]
    W2 = [[ 0.01744812 -0.00761207]]
    b2 = [[ 0.]]
    

    两层的神经网络测试已经完毕了,那么对于一个L层的神经网络而言呢?初始化会是什么样的?

    假设X(输入数据)的维度为(12288,209):

    <tr>
        <td>  </td> 
        <td> W的维度 </td> 
        <td> b的维度  </td> 
        <td> 激活值的计算</td>
        <td> 激活值的维度</td> 
    <tr>
    
    <tr>
        <td> 第 1 层 </td> 
        <td> $(n^{[1]},12288)$ </td> 
        <td> $(n^{[1]},1)$ </td> 
        <td> $Z^{[1]} = W^{[1]}  X + b^{[1]} $ </td> 
        
        <td> $(n^{[1]},209)$ </td> 
    <tr>
    
    <tr>
        <td> 第 2 层 </td> 
        <td> $(n^{[2]}, n^{[1]})$  </td> 
        <td> $(n^{[2]},1)$ </td> 
        <td>$Z^{[2]} = W^{[2]} A^{[1]} + b^{[2]}$ </td> 
        <td> $(n^{[2]}, 209)$ </td> 
    <tr>
    
       <tr>
        <td> $\vdots$ </td> 
        <td> $\vdots$  </td> 
        <td> $\vdots$  </td> 
        <td> $\vdots$</td> 
        <td> $\vdots$  </td> 
    <tr>
    
    第 L-1 层 $(n^{[L-1]}, n^{[L-2]})$ $(n^{[L-1]}, 1)$ $Z^{[L-1]} = W^{[L-1]} A^{[L-2]} + b^{[L-1]}$ $(n^{[L-1]}, 209)$ 第 L 层 $(n^{[L]}, n^{[L-1]})$ $(n^{[L]}, 1)$ $Z^{[L]} = W^{[L]} A^{[L-1]} + b^{[L]}$ $(n^{[L]}, 209)$

    当然,矩阵的计算方法还是要说一下的:
    W=[jklmnopqr]      X=[abcdefghi]      b=[stu](1) W = \begin{bmatrix} j & k & l\\ m & n & o \\ p & q & r \end{bmatrix}\;\;\; X = \begin{bmatrix} a & b & c\\ d & e & f \\ g & h & i \end{bmatrix} \;\;\; b =\begin{bmatrix} s \\ t \\ u \end{bmatrix}\tag{1}

    如果要计算 WX+bWX + b 的话,计算方法是这样的:

    WX+b=[(ja+kd+lg)+s(jb+ke+lh)+s(jc+kf+li)+s(ma+nd+og)+t(mb+ne+oh)+t(mc+nf+oi)+t(pa+qd+rg)+u(pb+qe+rh)+u(pc+qf+ri)+u](2) WX + b = \begin{bmatrix} (ja + kd + lg) + s & (jb + ke + lh) + s & (jc + kf + li)+ s\\ (ma + nd + og) + t & (mb + ne + oh) + t & (mc + nf + oi) + t\\ (pa + qd + rg) + u & (pb + qe + rh) + u & (pc + qf + ri)+ u \end{bmatrix}\tag{2}

    在实际中,也不需要你去做这么复杂的运算,我们来看一下它是怎样计算的吧:

    def initialize_parameters_deep(layers_dims):
        """
        此函数是为了初始化多层网络参数而使用的函数。
        参数:
            layers_dims - 包含我们网络中每个图层的节点数量的列表
        
        返回:
            parameters - 包含参数“W1”,“b1”,...,“WL”,“bL”的字典:
                         W1 - 权重矩阵,维度为(layers_dims [1],layers_dims [1-1])
                         bl - 偏向量,维度为(layers_dims [1],1)
        """
        np.random.seed(3)
        parameters = {}
        L = len(layers_dims)
        
        for l in range(1,L):
    	    parameters["W" + str(l)] = np.random.randn(layers_dims[l], layers_dims[l - 1]) / np.sqrt(layers_dims[l - 1])
            parameters["b" + str(l)] = np.zeros((layers_dims[l], 1))
            
            #确保我要的数据的格式是正确的
            assert(parameters["W" + str(l)].shape == (layers_dims[l], layers_dims[l-1]))
            assert(parameters["b" + str(l)].shape == (layers_dims[l], 1))
            
        return parameters
    

    测试一下:

    #测试initialize_parameters_deep
    print("==============测试initialize_parameters_deep==============")
    layers_dims = [5,4,3]
    parameters = initialize_parameters_deep(layers_dims)
    print("W1 = " + str(parameters["W1"]))
    print("b1 = " + str(parameters["b1"]))
    print("W2 = " + str(parameters["W2"]))
    print("b2 = " + str(parameters["b2"]))
    

    测试结果:

    ==============测试initialize_parameters_deep==============
    W1 = [[ 0.01788628  0.0043651   0.00096497 -0.01863493 -0.00277388]
     [-0.00354759 -0.00082741 -0.00627001 -0.00043818 -0.00477218]
     [-0.01313865  0.00884622  0.00881318  0.01709573  0.00050034]
     [-0.00404677 -0.0054536  -0.01546477  0.00982367 -0.01101068]]
    b1 = [[ 0.]
     [ 0.]
     [ 0.]
     [ 0.]]
    W2 = [[-0.01185047 -0.0020565   0.01486148  0.00236716]
     [-0.01023785 -0.00712993  0.00625245 -0.00160513]
     [-0.00768836 -0.00230031  0.00745056  0.01976111]]
    b2 = [[ 0.]
     [ 0.]
     [ 0.]]
    

    我们分别构建了两层和多层神经网络的初始化参数的函数,现在我们开始构建前向传播函数。


    前向传播函数

    前向传播有以下三个步骤

    • LINEAR
    • LINEAR - >ACTIVATION,其中激活函数将会使用ReLU或Sigmoid。
    • [LINEAR - > RELU] ×(L-1) - > LINEAR - > SIGMOID(整个模型)

    线性正向传播模块(向量化所有示例)使用公式(3)进行计算:
    Z[l]=W[l]A[l1]+b[l](3)Z^{[l]} = W^{[l]}A^{[l-1]} +b^{[l]}\tag{3}

    线性部分【LINEAR】

    前向传播中,线性部分计算如下:

    def linear_forward(A,W,b):
        """
        实现前向传播的线性部分。
    
        参数:
            A - 来自上一层(或输入数据)的激活,维度为(上一层的节点数量,示例的数量)
            W - 权重矩阵,numpy数组,维度为(当前图层的节点数量,前一图层的节点数量)
            b - 偏向量,numpy向量,维度为(当前图层节点数量,1)
    
        返回:
             Z - 激活功能的输入,也称为预激活参数
             cache - 一个包含“A”,“W”和“b”的字典,存储这些变量以有效地计算后向传递
        """
        Z = np.dot(W,A) + b
        assert(Z.shape == (W.shape[0],A.shape[1]))
        cache = (A,W,b)
         
        return Z,cache
    

    测试一下线性部分:

    #测试linear_forward
    print("==============测试linear_forward==============")
    A,W,b = testCases.linear_forward_test_case()
    Z,linear_cache = linear_forward(A,W,b)
    print("Z = " + str(Z))
    

    测试结果:

    ==============测试linear_forward==============
    Z = [[ 3.26295337 -1.23429987]]
    

    我们前向传播的单层计算完成了一半啦!我们来开始构建后半部分,如果你不知道我在说啥,请往上翻到【开始之前】仔细看看吧~

    线性激活部分【LINEAR - >ACTIVATION】

      为了更方便,我们将把两个功能(线性和激活)分组为一个功能(LINEAR-> ACTIVATION)。 因此,我们将实现一个执行LINEAR前进步骤,然后执行ACTIVATION前进步骤的功能。我们来看看这激活函数的数学实现吧~

    • Sigmoid: σ(Z)=σ(WA+b)=11+e(WA+b)\sigma(Z) = \sigma(W A + b) = \frac{1}{ 1 + e^{-(W A + b)}}
    • ReLU: A=RELU(Z)=max(0,Z)A = RELU(Z) = max(0, Z)

      我们为了实现LINEAR->ACTIVATION这个步骤, 使用的公式是:A[l]=g(Z[l])=g(W[l]A[l1]+b[l])A^{[l]} = g(Z^{[l]}) = g(W^{[l]}A^{[l-1]} +b^{[l]}),其中,函数g会是sigmoid() 或者是 relu(),当然,sigmoid()只在输出层使用,现在我们正式构建前向线性激活部分。

    def linear_activation_forward(A_prev,W,b,activation):
        """
        实现LINEAR-> ACTIVATION 这一层的前向传播
    
        参数:
            A_prev - 来自上一层(或输入层)的激活,维度为(上一层的节点数量,示例数)
            W - 权重矩阵,numpy数组,维度为(当前层的节点数量,前一层的大小)
            b - 偏向量,numpy阵列,维度为(当前层的节点数量,1)
            activation - 选择在此层中使用的激活函数名,字符串类型,【"sigmoid" | "relu"】
    
        返回:
            A - 激活函数的输出,也称为激活后的值
            cache - 一个包含“linear_cache”和“activation_cache”的字典,我们需要存储它以有效地计算后向传递
        """
        
        if activation == "sigmoid":
            Z, linear_cache = linear_forward(A_prev, W, b)
            A, activation_cache = sigmoid(Z)
        elif activation == "relu":
            Z, linear_cache = linear_forward(A_prev, W, b)
            A, activation_cache = relu(Z)
        
        assert(A.shape == (W.shape[0],A_prev.shape[1]))
        cache = (linear_cache,activation_cache)
        
        return A,cache
    

    测试一下:

    #测试linear_activation_forward
    print("==============测试linear_activation_forward==============")
    A_prev, W,b = testCases.linear_activation_forward_test_case()
    
    A, linear_activation_cache = linear_activation_forward(A_prev, W, b, activation = "sigmoid")
    print("sigmoid,A = " + str(A))
    
    A, linear_activation_cache = linear_activation_forward(A_prev, W, b, activation = "relu")
    print("ReLU,A = " + str(A))
    
    

    测试结果:

    ==============测试linear_activation_forward==============
    sigmoid,A = [[ 0.96890023  0.11013289]]
    ReLU,A = [[ 3.43896131  0.        ]]
    

      我们把两层模型需要的前向传播函数做完了,那多层网络模型的前向传播是怎样的呢?我们调用上面的那两个函数来实现它,为了在实现L层神经网络时更加方便,我们需要一个函数来复制前一个函数(带有RELU的linear_activation_forward)L-1次,然后用一个带有SIGMOID的linear_activation_forward跟踪它,我们来看一下它的结构是怎样的:
    [LINEAR -> RELU]  ××  (L-1) -> LINEAR -> SIGMOID model

    **Figure 2** : *[LINEAR -> RELU] $\times$ (L-1) -> LINEAR -> SIGMOID* model

    在下面的代码中,AL表示A[L]=σ(Z[L])=σ(W[L]A[L1]+b[L])A^{[L]} = \sigma(Z^{[L]}) = \sigma(W^{[L]} A^{[L-1]} + b^{[L]}). (也可称作 Yhat,数学表示为 Y^\hat{Y}.)

    多层模型的前向传播计算模型代码如下:

    def L_model_forward(X,parameters):
        """
        实现[LINEAR-> RELU] *(L-1) - > LINEAR-> SIGMOID计算前向传播,也就是多层网络的前向传播,为后面每一层都执行LINEAR和ACTIVATION
        
        参数:
            X - 数据,numpy数组,维度为(输入节点数量,示例数)
            parameters - initialize_parameters_deep()的输出
        
        返回:
            AL - 最后的激活值
            caches - 包含以下内容的缓存列表:
                     linear_relu_forward()的每个cache(有L-1个,索引为从0到L-2)
                     linear_sigmoid_forward()的cache(只有一个,索引为L-1)
        """
        caches = []
        A = X
        L = len(parameters) // 2
        for l in range(1,L):
            A_prev = A 
            A, cache = linear_activation_forward(A_prev, parameters['W' + str(l)], parameters['b' + str(l)], "relu")
            caches.append(cache)
        
        AL, cache = linear_activation_forward(A, parameters['W' + str(L)], parameters['b' + str(L)], "sigmoid")
        caches.append(cache)
        
        assert(AL.shape == (1,X.shape[1]))
        
        return AL,caches
    

    测试一下:

    #测试L_model_forward
    print("==============测试L_model_forward==============")
    X,parameters = testCases.L_model_forward_test_case()
    AL,caches = L_model_forward(X,parameters)
    print("AL = " + str(AL))
    print("caches 的长度为 = " + str(len(caches)))
    

    测试结果:

    ==============测试L_model_forward==============
    AL = [[ 0.17007265  0.2524272 ]]
    caches 的长度为 = 2
    

    计算成本

    我们已经把这两个模型的前向传播部分完成了,我们需要计算成本(误差),以确定它到底有没有在学习,成本的计算公式如下:
    1mi=1m(y(i)log(a[L](i))+(1y(i))log(1a[L](i)))(4)-\frac{1}{m} \sum\limits_{i = 1}^{m} (y^{(i)}\log\left(a^{[L] (i)}\right) + (1-y^{(i)})\log\left(1- a^{[L](i)}\right)) \tag{4}

    def compute_cost(AL,Y):
        """
        实施等式(4)定义的成本函数。
    
        参数:
            AL - 与标签预测相对应的概率向量,维度为(1,示例数量)
            Y - 标签向量(例如:如果不是猫,则为0,如果是猫则为1),维度为(1,数量)
    
        返回:
            cost - 交叉熵成本
        """
        m = Y.shape[1]
        cost = -np.sum(np.multiply(np.log(AL),Y) + np.multiply(np.log(1 - AL), 1 - Y)) / m
            
        cost = np.squeeze(cost)
        assert(cost.shape == ())
    
        return cost
    

    测试一下:

    #测试compute_cost
    print("==============测试compute_cost==============")
    Y,AL = testCases.compute_cost_test_case()
    print("cost = " + str(compute_cost(AL, Y)))
    

    测试结果:

    ==============测试compute_cost==============
    cost = 0.414931599615
    

    我们已经把误差值计算出来了,现在开始进行反向传播


    反向传播

    反向传播用于计算相对于参数的损失函数的梯度,我们来看看向前和向后传播的流程图:
    Forward and Backward propagation
    流程图有了,我们再来看一看对于线性的部分的公式:
    Linear Pic
    我们需要使用dZ[l]dZ^{[l]}来计算三个输出 (dW[l],db[l],dA[l])(dW^{[l]}, db^{[l]}, dA^{[l]}),下面三个公式是我们要用到的:
    dW[l]=LW[l]=1mdZ[l]A[l1]T(5) dW^{[l]} = \frac{\partial \mathcal{L} }{\partial W^{[l]}} = \frac{1}{m} dZ^{[l]} A^{[l-1] T} \tag{5}
    db[l]=Lb[l]=1mi=1mdZ[l](i)(6) db^{[l]} = \frac{\partial \mathcal{L} }{\partial b^{[l]}} = \frac{1}{m} \sum_{i = 1}^{m} dZ^{[l](i)}\tag{6}
    dA[l1]=LA[l1]=W[l]TdZ[l](7) dA^{[l-1]} = \frac{\partial \mathcal{L} }{\partial A^{[l-1]}} = W^{[l] T} dZ^{[l]} \tag{7}

    与前向传播类似,我们有需要使用三个步骤来构建反向传播:

    • LINEAR 后向计算
    • LINEAR -> ACTIVATION 后向计算,其中ACTIVATION 计算Relu或者Sigmoid 的结果
    • [LINEAR -> RELU] ×\times (L-1) -> LINEAR -> SIGMOID 后向计算 (整个模型)

    线性部分【LINEAR backward】

    我们来实现后向传播线性部分:

    def linear_backward(dZ,cache):
        """
        为单层实现反向传播的线性部分(第L层)
    
        参数:
             dZ - 相对于(当前第l层的)线性输出的成本梯度
             cache - 来自当前层前向传播的值的元组(A_prev,W,b)
    
        返回:
             dA_prev - 相对于激活(前一层l-1)的成本梯度,与A_prev维度相同
             dW - 相对于W(当前层l)的成本梯度,与W的维度相同
             db - 相对于b(当前层l)的成本梯度,与b维度相同
        """
        A_prev, W, b = cache
        m = A_prev.shape[1]
        dW = np.dot(dZ, A_prev.T) / m
        db = np.sum(dZ, axis=1, keepdims=True) / m
        dA_prev = np.dot(W.T, dZ)
        
        assert (dA_prev.shape == A_prev.shape)
        assert (dW.shape == W.shape)
        assert (db.shape == b.shape)
        
        return dA_prev, dW, db
    

    测试一下:

    #测试linear_backward
    print("==============测试linear_backward==============")
    dZ, linear_cache = testCases.linear_backward_test_case()
    
    dA_prev, dW, db = linear_backward(dZ, linear_cache)
    print ("dA_prev = "+ str(dA_prev))
    print ("dW = " + str(dW))
    print ("db = " + str(db))
    

    测试结果:

    ==============测试linear_backward==============
    dA_prev = [[ 0.51822968 -0.19517421]
     [-0.40506361  0.15255393]
     [ 2.37496825 -0.89445391]]
    dW = [[-0.10076895  1.40685096  1.64992505]]
    db = [[ 0.50629448]]
    

    线性激活部分【LINEAR -> ACTIVATION backward】

    为了帮助你实现linear_activation_backward,我们提供了两个后向函数:

    • sigmoid_backward:实现了sigmoid()函数的反向传播,你可以这样调用它:
    dZ = sigmoid_backward(dA, activation_cache)
    
    • relu_backward: 实现了relu()函数的反向传播,你可以这样调用它:
    dZ = relu_backward(dA, activation_cache)
    

    如果 g(.)g(.) 是激活函数, 那么sigmoid_backwardrelu_backward 这样计算:
    dZ[l]=dA[l]g(Z[l])(8)dZ^{[l]} = dA^{[l]} * g'(Z^{[l]}) \tag{8}.

    我们先在正式开始实现后向线性激活:

    def linear_activation_backward(dA,cache,activation="relu"):
        """
        实现LINEAR-> ACTIVATION层的后向传播。
        
        参数:
             dA - 当前层l的激活后的梯度值
             cache - 我们存储的用于有效计算反向传播的值的元组(值为linear_cache,activation_cache)
             activation - 要在此层中使用的激活函数名,字符串类型,【"sigmoid" | "relu"】
        返回:
             dA_prev - 相对于激活(前一层l-1)的成本梯度值,与A_prev维度相同
             dW - 相对于W(当前层l)的成本梯度值,与W的维度相同
             db - 相对于b(当前层l)的成本梯度值,与b的维度相同
        """
        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)
        
        return dA_prev,dW,db
    

    测试一下:

    #测试linear_activation_backward
    print("==============测试linear_activation_backward==============")
    AL, linear_activation_cache = testCases.linear_activation_backward_test_case()
     
    dA_prev, dW, db = linear_activation_backward(AL, linear_activation_cache, activation = "sigmoid")
    print ("sigmoid:")
    print ("dA_prev = "+ str(dA_prev))
    print ("dW = " + str(dW))
    print ("db = " + str(db) + "\n")
     
    dA_prev, dW, db = linear_activation_backward(AL, linear_activation_cache, activation = "relu")
    print ("relu:")
    print ("dA_prev = "+ str(dA_prev))
    print ("dW = " + str(dW))
    print ("db = " + str(db))
    

    测试结果:

    ==============测试linear_activation_backward==============
    sigmoid:
    dA_prev = [[ 0.11017994  0.01105339]
     [ 0.09466817  0.00949723]
     [-0.05743092 -0.00576154]]
    dW = [[ 0.10266786  0.09778551 -0.01968084]]
    db = [[-0.05729622]]
    
    relu:
    dA_prev = [[ 0.44090989 -0.        ]
     [ 0.37883606 -0.        ]
     [-0.2298228   0.        ]]
    dW = [[ 0.44513824  0.37371418 -0.10478989]]
    db = [[-0.20837892]]
    

    我们已经把两层模型的后向计算完成了,对于多层模型我们也需要这两个函数来完成,我们来看一下流程图:
    Backward pass

      在之前的前向计算中,我们存储了一些包含包含(X,W,b和z)的cache,在犯下那个船舶中,我们将会使用它们来计算梯度值,所以,在L层模型中,我们需要从L层遍历所有的隐藏层,在每一步中,我们需要使用那一层的cache值来进行反向传播。
       上面我们提到了A[L]A^{[L]},它属于输出层,A[L]=σ(Z[L])A^{[L]} = \sigma(Z^{[L]}),所以我们需要计算dAL,我们可以使用下面的代码来计算它:

    dAL = - (np.divide(Y, AL) - np.divide(1 - Y, 1 - AL))
    

       计算完了以后,我们可以使用此激活后的梯度dAL继续向后计算,我们这就开始构建多层模型向后传播函数:

    def L_model_backward(AL,Y,caches):
        """
        对[LINEAR-> RELU] *(L-1) - > LINEAR - > SIGMOID组执行反向传播,就是多层网络的向后传播
        
        参数:
         AL - 概率向量,正向传播的输出(L_model_forward())
         Y - 标签向量(例如:如果不是猫,则为0,如果是猫则为1),维度为(1,数量)
         caches - 包含以下内容的cache列表:
                     linear_activation_forward("relu")的cache,不包含输出层
                     linear_activation_forward("sigmoid")的cache
        
        返回:
         grads - 具有梯度值的字典
                  grads [“dA”+ str(l)] = ...
                  grads [“dW”+ str(l)] = ...
                  grads [“db”+ str(l)] = ...
        """
        grads = {}
        L = len(caches)
        m = AL.shape[1]
        Y = Y.reshape(AL.shape)
        dAL = - (np.divide(Y, AL) - np.divide(1 - Y, 1 - AL))
        
        current_cache = caches[L-1]
        grads["dA" + str(L)], grads["dW" + str(L)], grads["db" + str(L)] = linear_activation_backward(dAL, current_cache, "sigmoid")
        
        for l in reversed(range(L-1)):
            current_cache = caches[l]
            dA_prev_temp, dW_temp, db_temp = linear_activation_backward(grads["dA" + str(l + 2)], current_cache, "relu")
            grads["dA" + str(l + 1)] = dA_prev_temp
            grads["dW" + str(l + 1)] = dW_temp
            grads["db" + str(l + 1)] = db_temp
        
        return grads
    

    测试一下:

    #测试L_model_backward
    print("==============测试L_model_backward==============")
    AL, Y_assess, caches = testCases.L_model_backward_test_case()
    grads = L_model_backward(AL, Y_assess, caches)
    print ("dW1 = "+ str(grads["dW1"]))
    print ("db1 = "+ str(grads["db1"]))
    print ("dA1 = "+ str(grads["dA1"]))
    

    测试结果:

    ==============测试L_model_backward==============
    dW1 = [[ 0.41010002  0.07807203  0.13798444  0.10502167]
     [ 0.          0.          0.          0.        ]
     [ 0.05283652  0.01005865  0.01777766  0.0135308 ]]
    db1 = [[-0.22007063]
     [ 0.        ]
     [-0.02835349]]
    dA1 = [[ 0.          0.52257901]
     [ 0.         -0.3269206 ]
     [ 0.         -0.32070404]
     [ 0.         -0.74079187]]
    

    更新参数

    我们把向前向后传播都完成了,现在我们就开始更新参数,当然,我们来看看更新参数的公式吧~

    W[l]=W[l]α dW[l](9) W^{[l]} = W^{[l]} - \alpha \text{ } dW^{[l]} \tag{9}
    b[l]=b[l]α db[l](10) b^{[l]} = b^{[l]} - \alpha \text{ } db^{[l]} \tag{10}

    其中 α\alpha 是学习率。

    def update_parameters(parameters, grads, learning_rate):
        """
        使用梯度下降更新参数
        
        参数:
         parameters - 包含你的参数的字典
         grads - 包含梯度值的字典,是L_model_backward的输出
        
        返回:
         parameters - 包含更新参数的字典
                       参数[“W”+ str(l)] = ...
                       参数[“b”+ str(l)] = ...
        """
        L = len(parameters) // 2 #整除
        for l in range(L):
            parameters["W" + str(l + 1)] = parameters["W" + str(l + 1)] - learning_rate * grads["dW" + str(l + 1)]
            parameters["b" + str(l + 1)] = parameters["b" + str(l + 1)] - learning_rate * grads["db" + str(l + 1)]
            
        return parameters
    

    测试一下:

    #测试update_parameters
    print("==============测试update_parameters==============")
    parameters, grads = testCases.update_parameters_test_case()
    parameters = update_parameters(parameters, grads, 0.1)
     
    print ("W1 = "+ str(parameters["W1"]))
    print ("b1 = "+ str(parameters["b1"]))
    print ("W2 = "+ str(parameters["W2"]))
    print ("b2 = "+ str(parameters["b2"]))
    
    

    测试结果:

    ==============测试update_parameters==============
    W1 = [[-0.59562069 -0.09991781 -2.14584584  1.82662008]
     [-1.76569676 -0.80627147  0.51115557 -1.18258802]
     [-1.0535704  -0.86128581  0.68284052  2.20374577]]
    b1 = [[-0.04659241]
     [-1.28888275]
     [ 0.53405496]]
    W2 = [[-0.55569196  0.0354055   1.32964895]]
    b2 = [[-0.84610769]]
    

      至此为止,我们已经实现该神经网络中所有需要的函数。接下来,我们将这些方法组合在一起,构成一个神经网络类,可以方便的使用。


    搭建两层神经网络

    一个两层的神经网络模型图如下:
    2-layer neural network.

    该模型可以概括为: **INPUT -> LINEAR -> RELU -> LINEAR -> SIGMOID -> OUTPUT**

    我们正式开始构建两层的神经网络:

    def two_layer_model(X,Y,layers_dims,learning_rate=0.0075,num_iterations=3000,print_cost=False,isPlot=True):
        """
        实现一个两层的神经网络,【LINEAR->RELU】 -> 【LINEAR->SIGMOID】
        参数:
            X - 输入的数据,维度为(n_x,例子数)
            Y - 标签,向量,0为非猫,1为猫,维度为(1,数量)
            layers_dims - 层数的向量,维度为(n_y,n_h,n_y)
            learning_rate - 学习率
            num_iterations - 迭代的次数
            print_cost - 是否打印成本值,每100次打印一次
            isPlot - 是否绘制出误差值的图谱
        返回:
            parameters - 一个包含W1,b1,W2,b2的字典变量
        """
        np.random.seed(1)
        grads = {}
        costs = []
        (n_x,n_h,n_y) = layers_dims
        
        """
        初始化参数
        """
        parameters = initialize_parameters(n_x, n_h, n_y)
        
        W1 = parameters["W1"]
        b1 = parameters["b1"]
        W2 = parameters["W2"]
        b2 = parameters["b2"]
        
        """
        开始进行迭代
        """
        for i in range(0,num_iterations):
            #前向传播
            A1, cache1 = linear_activation_forward(X, W1, b1, "relu")
            A2, cache2 = linear_activation_forward(A1, W2, b2, "sigmoid")
            
            #计算成本
            cost = compute_cost(A2,Y)
            
            #后向传播
            ##初始化后向传播
            dA2 = - (np.divide(Y, A2) - np.divide(1 - Y, 1 - A2))
            
            ##向后传播,输入:“dA2,cache2,cache1”。 输出:“dA1,dW2,db2;还有dA0(未使用),dW1,db1”。
            dA1, dW2, db2 = linear_activation_backward(dA2, cache2, "sigmoid")
            dA0, dW1, db1 = linear_activation_backward(dA1, cache1, "relu")
            
            ##向后传播完成后的数据保存到grads
            grads["dW1"] = dW1
            grads["db1"] = db1
            grads["dW2"] = dW2
            grads["db2"] = db2
            
            #更新参数
            parameters = update_parameters(parameters,grads,learning_rate)
            W1 = parameters["W1"]
            b1 = parameters["b1"]
            W2 = parameters["W2"]
            b2 = parameters["b2"]
            
            #打印成本值,如果print_cost=False则忽略
            if i % 100 == 0:
                #记录成本
                costs.append(cost)
                #是否打印成本值
                if print_cost:
                    print("第", i ,"次迭代,成本值为:" ,np.squeeze(cost))
        #迭代完成,根据条件绘制图
        if isPlot:
            plt.plot(np.squeeze(costs))
            plt.ylabel('cost')
            plt.xlabel('iterations (per tens)')
            plt.title("Learning rate =" + str(learning_rate))
            plt.show()
        
        #返回parameters
        return parameters
    

      我们现在开始加载数据集,图像数据集的处理可以参照:【中文】【吴恩达课后编程作业】Course 1 - 神经网络和深度学习 - 第二周作业,就连数据集也是一样的。

    train_set_x_orig , train_set_y , test_set_x_orig , test_set_y , classes = lr_utils.load_dataset()
    
    train_x_flatten = train_set_x_orig.reshape(train_set_x_orig.shape[0], -1).T 
    test_x_flatten = test_set_x_orig.reshape(test_set_x_orig.shape[0], -1).T
    
    train_x = train_x_flatten / 255
    train_y = train_set_y
    test_x = test_x_flatten / 255
    test_y = test_set_y
    
    

    数据集加载完成,开始正式训练:

    n_x = 12288
    n_h = 7
    n_y = 1
    layers_dims = (n_x,n_h,n_y)
    
    parameters = two_layer_model(train_x, train_set_y, layers_dims = (n_x, n_h, n_y), num_iterations = 2500, print_cost=True,isPlot=True)
    
    
    

    训练结果:

    0 次迭代,成本值为: 0.69304973566100 次迭代,成本值为: 0.646432095343200 次迭代,成本值为: 0.632514064791300 次迭代,成本值为: 0.601502492035400 次迭代,成本值为: 0.560196631161500 次迭代,成本值为: 0.515830477276600 次迭代,成本值为: 0.475490131394700 次迭代,成本值为: 0.433916315123800 次迭代,成本值为: 0.40079775362900 次迭代,成本值为: 0.3580705011321000 次迭代,成本值为: 0.3394281538371100 次迭代,成本值为: 0.305275363621200 次迭代,成本值为: 0.2749137728211300 次迭代,成本值为: 0.2468176821061400 次迭代,成本值为: 0.1985073503751500 次迭代,成本值为: 0.1744831811261600 次迭代,成本值为: 0.1708076297811700 次迭代,成本值为: 0.1130652456221800 次迭代,成本值为: 0.09629426845941900 次迭代,成本值为: 0.08342617959732000 次迭代,成本值为: 0.07439078704322100 次迭代,成本值为: 0.06630748132272200 次迭代,成本值为: 0.05919329501042300 次迭代,成本值为: 0.05336140348562400 次迭代,成本值为: 0.0485547856288
    

    two layers model train result

    迭代完成之后我们就可以进行预测了,预测函数如下:

    def predict(X, y, parameters):
        """
        该函数用于预测L层神经网络的结果,当然也包含两层
        
        参数:
         X - 测试集
         y - 标签
         parameters - 训练模型的参数
        
        返回:
         p - 给定数据集X的预测
        """
        
        m = X.shape[1]
        n = len(parameters) // 2 # 神经网络的层数
        p = np.zeros((1,m))
        
        #根据参数前向传播
        probas, caches = L_model_forward(X, parameters)
        
        for i in range(0, probas.shape[1]):
            if probas[0,i] > 0.5:
                p[0,i] = 1
            else:
                p[0,i] = 0
        
        print("准确度为: "  + str(float(np.sum((p == y))/m)))
            
        return p
    

    预测函数构建好了我们就开始预测,查看训练集和测试集的准确性:

    predictions_train = predict(train_x, train_y, parameters) #训练集
    predictions_test = predict(test_x, test_y, parameters) #测试集
    

    预测结果:

    准确度为: 1.0
    准确度为: 0.72
    

    这样看来,我的测试集的准确度要比上一次(【中文】【吴恩达课后编程作业】Course 1 - 神经网络和深度学习 - 第二周作业)高一些,上次的是70%,这次是72%,那如果我使用更多层的圣经网络呢?


    搭建多层神经网络

    我们首先来看看多层的网络的结构吧~
    L layers neural networ

    def L_layer_model(X, Y, layers_dims, learning_rate=0.0075, num_iterations=3000, print_cost=False,isPlot=True):
        """
        实现一个L层神经网络:[LINEAR-> RELU] *(L-1) - > LINEAR-> SIGMOID。
        
        参数:
    	    X - 输入的数据,维度为(n_x,例子数)
            Y - 标签,向量,0为非猫,1为猫,维度为(1,数量)
            layers_dims - 层数的向量,维度为(n_y,n_h,···,n_h,n_y)
            learning_rate - 学习率
            num_iterations - 迭代的次数
            print_cost - 是否打印成本值,每100次打印一次
            isPlot - 是否绘制出误差值的图谱
        
        返回:
         parameters - 模型学习的参数。 然后他们可以用来预测。
        """
        np.random.seed(1)
        costs = []
        
        parameters = initialize_parameters_deep(layers_dims)
        
        for i in range(0,num_iterations):
            AL , caches = L_model_forward(X,parameters)
            
            cost = compute_cost(AL,Y)
            
            grads = L_model_backward(AL,Y,caches)
            
            parameters = update_parameters(parameters,grads,learning_rate)
            
            #打印成本值,如果print_cost=False则忽略
            if i % 100 == 0:
                #记录成本
                costs.append(cost)
                #是否打印成本值
                if print_cost:
                    print("第", i ,"次迭代,成本值为:" ,np.squeeze(cost))
        #迭代完成,根据条件绘制图
        if isPlot:
            plt.plot(np.squeeze(costs))
            plt.ylabel('cost')
            plt.xlabel('iterations (per tens)')
            plt.title("Learning rate =" + str(learning_rate))
            plt.show()
        return parameters
    

      我们现在开始加载数据集,图像数据集的处理可以参照:【中文】【吴恩达课后编程作业】Course 1 - 神经网络和深度学习 - 第二周作业,就连数据集也是一样的。

    train_set_x_orig , train_set_y , test_set_x_orig , test_set_y , classes = lr_utils.load_dataset()
    
    train_x_flatten = train_set_x_orig.reshape(train_set_x_orig.shape[0], -1).T 
    test_x_flatten = test_set_x_orig.reshape(test_set_x_orig.shape[0], -1).T
    
    train_x = train_x_flatten / 255
    train_y = train_set_y
    test_x = test_x_flatten / 255
    test_y = test_set_y
    
    

    数据集加载完成,开始正式训练:

    layers_dims = [12288, 20, 7, 5, 1] #  5-layer model
    parameters = L_layer_model(train_x, train_y, layers_dims, num_iterations = 2500, print_cost = True,isPlot=True)
    
    

    训练结果:

    0 次迭代,成本值为: 0.715731513414100 次迭代,成本值为: 0.674737759347200 次迭代,成本值为: 0.660336543362300 次迭代,成本值为: 0.646288780215400 次迭代,成本值为: 0.629813121693500 次迭代,成本值为: 0.606005622927600 次迭代,成本值为: 0.569004126398700 次迭代,成本值为: 0.519796535044800 次迭代,成本值为: 0.464157167863900 次迭代,成本值为: 0.4084203004831000 次迭代,成本值为: 0.3731549921611100 次迭代,成本值为: 0.305723745731200 次迭代,成本值为: 0.2681015284771300 次迭代,成本值为: 0.2387247482771400 次迭代,成本值为: 0.2063226325791500 次迭代,成本值为: 0.1794388692751600 次迭代,成本值为: 0.1579873581881700 次迭代,成本值为: 0.1424041301231800 次迭代,成本值为: 0.1286516599791900 次迭代,成本值为: 0.1124431499822000 次迭代,成本值为: 0.08505631034972100 次迭代,成本值为: 0.05758391198612200 次迭代,成本值为: 0.0445675345472300 次迭代,成本值为: 0.0380827516662400 次迭代,成本值为: 0.0344107490184
    

    L_layer_model train result

    训练完成,我们看一下预测:

    pred_train = predict(train_x, train_y, parameters) #训练集
    pred_test = predict(test_x, test_y, parameters) #测试集
    

    预测结果:

    准确度为: 0.9952153110047847
    准确度为: 0.78
    

    就准确度而言,从70%到72%再到78%,可以看到的是准确度在一点点增加,当然,你也可以手动的去调整layers_dims,准确度可能又会提高一些。


    分析

    我们可以看一看有哪些东西在L层模型中被错误地标记了,导致准确率没有提高。

    def print_mislabeled_images(classes, X, y, p):
        """
    	绘制预测和实际不同的图像。
    	    X - 数据集
    	    y - 实际的标签
    	    p - 预测