吴恩达Coursera Deep Learning学习笔记 1 (下)

【写在前面】
作为一名终身学习实践者,我持之以恒地学习各种深度学习和机器学习的新知识。一个无聊的假日里我突然想到:反正一样要记笔记,为什么不把我学习的笔记写成博客供大家一起交流呢?于是,我就化身数据女侠,打算用博客分享的方式开启我的深度学习深度学习之旅。

本文仅供学习交流使用,侵权必删,不用于商业目的,转载须注明出处。

【学习笔记】

之前我们以逻辑回归 (logistic regression) 为例介绍了神经网络(吴恩达Coursera Deep Learning学习笔记 1 (上)),但它并没有隐藏层,所以并不算严格意义上的神经网络。在本文中,让我们随着Andrew一起深化神经网络,在sigmoid之前再增加一些ReLU神经元。最后我们会以疯狂收到AI科学家迷恋的可爱猫咪图案为例,用深度学习建立一个猫咪图案的识别模型。不过在这之前,让我们来看一下除了上次说到的sigmoid和ReLU之外,还有什么激活函数、他们之间又各有什么优劣——

激活函数 Activation Functions

1) sigmoid
除了在输出层(当输出是{0,1}的binary classification时)可能会用到之外,隐藏层中很少用到sigmoid,因为它的mean是0.5,同等情况下用均值为0的tanh函数取代。
2) tanh
其实就是sigmoid的shifted版本,但输出从(0, 1)变为(-1, 1),所以下一层的输入是centered,更受欢迎。
3) ReLU (Rectified Linear Unit)
吴恩达Coursera Deep Learning学习笔记 1 (上)中提到过ReLU激活函数,它在深度学习中比sigmoid和tanh更常用。这是因为当激活函数的输入z的值很大或很小的时候,sigmoid和tanh的梯度非常小,这会大大减缓梯度下降的学习速度。所以与sigmoid和tanh相比,ReLU的训练速度要快很多。
4) Leaky ReLU
Leaky ReLU比ReLU要表现得稍微好一丢丢但是实践中大家往往都用ReLU。


从浅神经网络到深神经网络

最开始的逻辑回归的例子中,并没有隐藏层。在接下来的课程中,Andrew分别介绍了1个隐藏层、2个隐藏层和L个隐藏层的神经网络。但是只要把前向传播和反向传播搞明白了,再加上之后会讲述的一些小撇步,就会发现其实都是换汤不换药。大家准备好了吗?和我一起深呼吸再一头扎进深度学习的海洋吧~

一般输入层是不计入神经网络的层数的,如果一个神经网络有L层,那么就意味着它有L-1个隐藏层和1个输出层。我们可以观察到输入层和输出层,但是不容易观察到中间数据是怎么变化的,因此在输入和输出层之间的部分叫隐藏层。

训练一个深神经网络大致分为以下步骤(吴恩达Coursera Deep Learning学习笔记 1 (上)也有详细说明):
1. 定义神经网络的结构(超参数)
2. 初始化参数
3. 循环迭代
    3.1 在前向传播中,分为linear forward和activation forward。在linear forward中,Z[l]=W[l]A[l−1]+b[l],其中A[0]=X;在activation forward中,A[l]=g(Z[l])。期间要储存W、b、Z的值。最后算出当前的Loss。
    3.2 在反向传播中,分为activation backward和linear backward。
    3.3 更新参数。
下图展现了一个L层的深度神经网络、其中L-1个隐藏层都用的是ReLU激活函数的训练步骤和过程。

一个L-1个ReLU隐藏层的训练过程

标注声明 Notations:
随着神经网络的模型越来越深,我们会有L-1个隐藏层,每一层都用小写的L即[l]标注为上标。z是上一层的输出(即这一层的输入)的线性组合,a是这一层通过激活函数对z的非线性变化,也叫激活值 (activation values)。训练数据中有m个样本,每一个样本都用(i)来标注为上标。每一个隐藏层l里都有n[l](上标内是小写的L不是1)个神经元,每一个神经元都用i标注为下标。

每一层、样本、单元的标注

超参数

随着我们的深度学习模型越来越复杂,我们要学习区分普通参数 (Parameters) 和超参数 (Hyperparameters)。 在上图的标注声明中出现的W和b是普通的参数,而超参数是指:

学习速率 (learning rate) alpha、循环次数 (# iterations)、隐藏层层数 (# hidden layers) L、每隐藏层中的神经元数量 (size of the hidden layers) n[l] ——上标内是小写的L不是1、激活函数 (choice of activation functions) g[l] ——上标内是小写的L不是1。除了这些超参数之外,之后还会学习到以下超参数:动量 (momentum)、小批量更新的样本数 (minibatch size)、正则化系数 (regularization weight),等等。

随机初始化 Random Initialization

吴恩达Coursera Deep Learning学习笔记 1 (上)中的逻辑回归 (logistic regression) 中没有隐藏层,所以将W直接初始化为0并无大碍。但是在初始化隐藏层的W时如果将每个神经元的权重都初始化为0,那么在之后的梯度下降中,每一个神经元的权重都会有相同的梯度和更新,这样的对称在梯度下降中永远无法打破,如此就算隐藏层中有一千一万个神经元,也只同于一个神经元。所以,为了打破这种对称的魔咒,在初始化参数时往往会加入一些微小的抖动,即用较小的随机数来初始化W,偏置项b可以初始化为0或者同样是较小的随机数。在Python中,可以用np.random.randn(a,b) * 0.01来随机地初始化a*b的W矩阵,并用np.zeros((a, 1))来初始化a*1的b矩阵。

为什么是0.01呢?同sigmoid和tanh中所说,数据科学家通常会从将W initialize为很小的随机数,防止训练的速度过缓。但是如果神经网络很深的话,0.01这样小的数字未必最理想。但是总体来说,人们还是倾向于从较小的参数开始训练。

承接上面的超参数,对于每个隐藏层中的神经元数量,我们可以将这几个超参数设定为layer_dims的array,如layer_dims = [n_x, 4,3,2,1] 说明输入的X有n_x个特征,第一层有4个神经元,第二层有3个神经元,第三层有2个,最后一个输出单元。有一个容易搞错的地方,就是W[l]是n[l]*n[l-1]的矩阵,b[l]是n[l]*1的矩阵。所以初始化W和b就可以写成:
for l in range(1, L):
parameters["W"+str(l)] = np.random.randn(layer_dims[l],layer_dims[l-1])*0.01
parameters["b"+str(l)] =np.zeros((layer_dims[l],1))
详见例2中的initialize_parameters_deep函数。

并不是很复杂有没有!那么,下面我们一起跟着Andrew来看几个神经网络的例子——


【例 1】用单个隐藏层的神经网络来分类平面数据

本例:4个神经元的隐藏层 (tanh) 加一个sigmoid的输出层

第三课的例子是Planar data classification with one hidden layer,即帮助大家搭建一个上图所示的浅神经网络(Shallow Neural Networks):一个4个单元的隐藏层 (tanh) 加一个sigmoid的输出层。

本例反向传播中各个梯度的计算(左为一个样本的情况,右为矢量化运算)

最后一步的prediction是用了0.5的cutoff,很简单:

将A2转化为Y-hat

最后的决策边界如下图,在训练数据上的精确度为90%,是不是比logistic regression表现强多啦?可见logistic regression不能学习到数据中的非线性关系,而神经网络可以(哪怕是本例中一个非常浅的神经网络)。

决策边界:一个有四个神经元的隐藏层的神经网络

其实本例中的模型也很简单,如果再复杂些可以做到更精确,但是可能会overfit,毕竟从上图中可以看出现有的模型已经抓住了数据中的大趋势。下面尝试了在隐藏层中设置不同个数的神经元,来看模型的精确度和决策边界是如何变化的:
Accuracy for 1 hidden units: 67.5 %
Accuracy for 2 hidden units: 67.25 %
Accuracy for 3 hidden units: 90.75 %
Accuracy for 4 hidden units: 90.5 %
Accuracy for 5 hidden units: 91.25 %
Accuracy for 20 hidden units: 90.0 %
Accuracy for 50 hidden units: 90.25 %

隐藏层有5个神经元vs.20个神经元的决策边界

可以看到,在训练数据上,5个神经元的精确度是最高的,而当神经元数超过20时,决策边界就显示有overfitting的情况了。不过没事,之后会学习正则化 (regularization),能使很复杂的神经网络都不会出现overfitting。

这个例子的代码很简单,就不贴了。


【例 2】L层深度神经网络

第四节课的例子有三个:第一是一个ReLU+sigmoid的浅层神经网络,是为了后面的例子做铺垫;第二个将其深化,用了L-1个ReLU层,输出层也是sigmoid;第三个例子就是用前两个神经网络训练猫咪识别模型[吐血]。我将L层的模型和其猫咪识别器的训练过程精简地说一下。

设计神经网络与随机初始化参数

下图就是我们要搭建的L层神经网络,不过在这之前,让我们先挑个lucky number方便以后重复训练结果^_^

np.random.seed(1)

本例:[线性组合 -> ReLU激活] * L-1次 -> 线性组合 -> sigmoid输出

其次,让我们设计一下我们的神经网络。因为激活函数已经确定用ReLU了,所以在本例中我们只需设计layer_dims,就能确定输入的维度、层数和每层的神经元数。

def initialize_parameters_deep(layer_dims):
    parameters = {}
    L = len(layer_dims)                      # 根据我们一开始设计的模型超参数,读取L(其实是L+1)
    for l in range(1, L):                       # 设定W1到W(L-1)和b1和b(L-1),一共有L-1层(其实是L)
        parameters['W' + str(l)] = np.random.randn(layer_dims[l], layer_dims[l-1]) * 0.01
        parameters['b' + str(l)] = np.zeros((layer_dims[l], 1))
    assert(parameters['W' + str(l)].shape == (layer_dims[l], layer_dims[l-1]))
    assert(parameters['b' + str(l)].shape == (layer_dims[l], 1))
    return parameters

和具体的数据结合,就知道了输入的维度和样本的数量。假设我们的训练数据中有209张图片,每张都是64*64像素,那么输入特征数n_x就是64*64*3 = 12288,m就是209,如下图:

每一层参数的维度

如果我们将W和b参数设为parameters,每一个初始化的W和b都是parameters这个list中的一个元素,那么L-1个循环隐藏层其实就是len(parameters)//2。下图是一个ReLU层加一个sigmoid层的一个loop,怎么将同样的计算复制到我们的L层深度神经网络中呢?

线性组合->ReLU->线性组合->sigmoid的前向与反向传播的例子

前向传播

前向传播分为linear forward和activation forward,前者计算Z,后者计算A=g(Z),g视激活函数的不同而不同。因为activation forward这步中包括了linear的值,所以名为linear_activation_forward函数。由于反向传播的梯度计算中会用到W、b、A的值,所以我们将每一个iteration中将每个神经元的这些值暂时储存在caches这个大列表中,再在下一轮循环中覆盖掉。代码如下:

def linear_forward(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

def linear_activation_forward(A_prev, W, b, activation):
    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之后,我们来定义这个L层神经网络的前向传播:

def L_model_forward(X, parameters):
    caches = []
    A = X
    L = len(parameters) // 2                  # 因为之前设定的parameters包含了每一层W和b的初始值,所以层数是这个列表长度的一半
    for l in range(1, L):                          # L-1个隐藏层用ReLU激活函数
        A_prev = A
        A, cache = linear_activation_forward(A_prev, parameters['W' + str(l)], parameters['b' + str(l)], activation = "relu")
        caches.append(cache)
    AL, cache = linear_activation_forward(A, parameters['W' + str(L)], parameters['b' + str(L)], activation = "sigmoid")            # 第L个层用sigmoid函数
    caches.append(cache)
    assert(AL.shape == (1,X.shape[1]))
    return AL, caches

前向传播的尽头是计算当前参数下的损失~不过正如在后面L_model_backward函数中看到的,我们这里直接计算dL/dAL,并不计算L,这里计算cost是为了在训练过程检查代价是不是在稳定下降,以确保我们使用了合适的学习率。

def compute_cost(AL, Y):
    m = Y.shape[1]
    cost = -np.sum(Y*np.log(AL) + (1-Y)*np.log(1-AL))/m
    cost = np.squeeze(cost)                            # 将类似于 [[17]] 的cost变成 17
    assert(cost.shape == ())
    return cost

反向传播

反向传播和前向传播的函数设计是对称的,但是会比前向传播复杂一丢丢,需要小心各种线性代数中的运算规则——这也是为什么在前向传播中我们都在return前加入了维度检查(assert + shape)。下图显示了每一个神经元在反向传播中的输入和输出。现在我们看到之前在前向传播中缓存的用处了。如果我不储存W和Z的值,我就没有办法在反向线性传播中计算dW,db同理。

反向线性传播中的输入和输出

def linear_backward(dZ, cache):
    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

上述的公式用线性代数表示为下图:

反向传播中参数梯度的计算

Andrew贴心地为大家提供了写好的函数:relu_backward和sigmoid_backward,如果我们自己写的话,需要在前向传播中储存A的值,否则在很多反向传播中就不知道dA/dZ,因为有些激活函数的导数是A的函数,比如sigmoid函数和tanh函数。

def linear_activation_backward(dA, cache, activation):
    linear_cache, activation_cache = cache
    if activation == "relu":
        dZ = relu_backward(dA, activation_cache)
        dA_prev, dW, db = linear_backward(dZ, linear_cache)
    elif activation == "sigmoid":
        dZ = sigmoid_backward(dA, activation_cache)
        dA_prev, dW, db = linear_backward(dZ, linear_cache)
    return dA_prev, dW, db

同前向传播一样,我们将dL/dAL反向传播,通过每一层的linear-activation backward构建整个完整的反向传播体系:

def L_model_backward(AL, Y, caches):
    grads = {}
    L = len(caches) # 层数
    m = AL.shape[1]
    Y = Y.reshape(AL.shape) # 改变Y的维度,确保其与AL的维度统一
    dAL = - (np.divide(Y, AL) - np.divide(1 - Y, 1 - AL)) # 代价函数对输出层输出AL的导数,就不计算具体的cost了
    current_cache = caches[L-1]
    grads["dA" + str(L)], grads["dW" + str(L)], grads["db" + str(L)] = linear_activation_backward(dAL, current_cache, activation = "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, activation = "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

一般现实工作中不会用线性代数如此折磨你,就算要自己一步一步这么写,也可以加入梯度检查等等来为你增添信心,具体以后再分享~

参数更新

至此我们已经在一个循环中计算出了当前W和b的梯度,最后就是用梯度下降的定义更新参数。在下一个例子中我们会看到如何用我们已经写好的每一步的函数,使用for loop执行梯度下降,最后得到训练好的模型。

def update_parameters(parameters, grads, learning_rate):
    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


【例 3】继续AI科学家对猫的执念……

下面我们用例2中的L层深度神经网络来识别一张图是不是猫咪[捂脸],因为代码有点多所以分成了2和3的两个例子。
假设train_x_orig是我们原始的输入,已经将图片像素数据提取并flatten为适合训练的数据,这里我们将每一个样本从64*64*3的输入变成一个12288*1的矢量,然后将值标准化到0-1之间:

train_x_flatten = train_x_orig.reshape(train_x_orig.shape[0], -1).T
train_x = train_x_flatten/255.

image2vector conversion

设计一个神经网络:layers_dims = [12288, 20, 7, 5, 1],即每个样本有12288个像素输入,第一层20个ReLU神经元,第二层7个,第三层5个,最后一个sigmoid。

L层的神经网络来识别图像中的喵喵

终于可以调用我们之前辛辛苦苦写好的函数啦!之前写的函数都是每一个iteration中的每一步骤,现在我们将每一个loop循环num_iterations次。

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)

这里的parameters就是训练好的参数,我们就可以用它来预测新的萌萌哒猫猫啦。
读取一张num_px*num_px图片的像素再将其RGB转换为num_px*num_px*3的方法,请注意这里的图片尺寸需和训练数据中的一样:

fname = "images/" + my_image
np.array(ndimage.imread(fname, flatten=False))
scipy.misc.imresize(image, size=(num_px,num_px)).reshape((num_px*num_px*3,1))

最后我们的模型在训练数据上的精确度为98.6%。然后我们就可以用类似于predict(test_x, test_y, parameters)这样的方法就能预测这个图片是不是一个喵喵啦!最后得到在训练数据上的精确度为80%,但是让我们来看看剩下20%没有正确预测的样本是什么样子的……

基础模型没有正确预测的样本例子

除了第五张姿势扭捏的猫猫外,2和4中的猫猫我们也没有很好地识别出来。不过不用担心,卷积神经网络 (Convolutional Neural Networks) 会比image2vector更适合于处理图片数据,所以敬请期待以后的更新!

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,222评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,455评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,720评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,568评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,696评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,879评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,028评论 3 409
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,773评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,220评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,550评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,697评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,360评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,002评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,782评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,010评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,433评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,587评论 2 350

推荐阅读更多精彩内容