这篇文章是一次学习DeepLearning.ai的Lesson 1的一次总结,起因是在几个月没有继续学习深度学习有关内容后,之前的一些基础已经有些忘却了,所以通过这次总结快速唤醒之前的记忆,也为了防止下一次忘记某个知识点需要重新去视频中寻找。

Loss Function - 损失函数

在课程中的解释如下,其核心在于通过正向传播(Forward Propagation)计算出y^\hat{y},也就通过神经网络预测的值,之后通过将损失函数不断变小使得y^\hat{y}不断趋近于yy(即正确的值),损失函数就是单个样例的损失/误差。最直观的损失函数为L(y^,y)=12(y^y)2L(\hat{y}, y) = \frac{1}{2}(\hat{y} - y)^{2}来实现,但是这个函数有一个缺陷就是由多个局部最小值,无法判断哪一个最小值是最优解。所以就使用了另一种比较常用的Loss Function: L(y^,y)=(ylogy^+(1y)log(1y^))L(\hat{y}, y) = -(y\log \hat{y} + (1-y)\log (1-\hat{y}))实现,其原理是:

  • 如果y=1y = 1这个公式就变成了L(y^,y)=logy^L(\hat{y}, y) = - \log\hat{y},为了使LF变小则需要logy^\log \hat{y}增大,又因为logx\log x是一个增函数则y^\hat{y}增大;
  • 如果y=0y = 0这个公式就变成了L(y^,y)=log(1y^)L(\hat{y}, y) = - \log (1 - \hat{y}),同样为了使LF变小则需要log(1y^)\log (1 - \hat{y})增大,则需要y^\hat{y}减小。

Cost Function - 代价函数

LF和CF描述是算是同一件事情,但是有一些细微的不同,Andrew Ng在视频中给出的定义是:

  • The loss function computes the error for a single training example;
  • The cost function is the average of the loss funcitons of the entire training set.

loss(error) function 是单个样例的损失/误差;而 cost function 是对数据集整体的误差描述,是选定参数 w 和 b 后对数据进行估计所要支付的代价,cost 是对所有数据的误差取平均得到的。所以CF的公式为J(w,b)=1mi=1mL(y^(i),y(i))J(w, b) = \frac{1}{m} \sum_{i = 1}^{m} L(\hat{y}^{(i)}, y^{(i)})

Error Back Propagation - BP算法

BP算法是神经网络中比较核心的概念了,主要包含Forward Propagation(FP)Backward Propagation(BP)两大核心函数。

函数详解

Forward Propagation

如果你的神经网络拥有nn层,则FP需要运行nn次:

  • 函数输入a[l1]a^{[l - 1]},也就是上一层运算的数据;
  • 函数输出a[l]a^{[l]},也就是下一层的函数输入;

主要步骤

  1. 计算z[l]=W[l]a[l1]+b[l]z^{[l]} = W^{[l]} a^{[l-1]} + b^{[l]},其中W[l]W^{[l]}是该层的参数,b[l]b^{[l]}是该层的bias参数。分析一下矩阵结构,假设ll层有n[l]n^{[l]}个神经元,l1l-1层有n[l1]n^{[l - 1]}个神经元,所以:
    • z[l]z^{[l]}的结构为(n[l],1)(n^{[l]}, 1)
    • a[l1]a^{[l-1]}的结构为(n[l1],1)(n^{[l - 1]}, 1)
    • 由此可以推断出W[l]W^{[l]}的结构为(n[l],n[l1])(n^{[l]}, n^{[l - 1]})
    • 同时也可以推断出b[l]b^{[l]}的结构为(n[l],1)(n^{[l]}, 1)
  2. 使用激活函数(Activation Function)最终计算出该层的最终输出a[l]=g[l](z[l])a^{[l]} = g^{[l]}(z^{[l]})
  3. 同时缓存住z[l]z^{[l]}W[l]W^{[l]}b[l]b^{[l]},这些数据都将用于BP中

Backward Propagation

如果你的神经网络拥有nn层,则BP需要运行nn次:

  • 函数输入da[l]\mathrm{d} a^{[l]},还需要有FP缓存过的z[l]z^{[l]}W[l]W^{[l]}b[l]b^{[l]}
  • 函数输出da[l1]\mathrm{d} a^{[l - 1]}dW[l]\mathrm{d} W^{[l]}db[l]\mathrm{d} b^{[l]}

如何计算da\mathrm{d} adz\mathrm{d} zdw\mathrm{d} wdb\mathrm{d} b

**[详见Lesson1的Week2#8和Week2#9]**虽然da[l]\mathrm{d} a^{[l]}是作为参数传入的,但是我们总需要计算一次da[l]\mathrm{d} a^{[l]},这将涉及到Logistic回归以及求导相关知识,可以参见带有Logistic Regression Derivatives的图示。注意:下列步骤没有使用向量化。

  • da=dL(a,y)da\mathrm{d} a = \frac{\mathrm{d} L(a, y)}{\mathrm{d} a},最后根据微积分相关知识可以化简为da[l]=ya+1y1a\mathrm{d} a^{[l]} = - \frac{y}{a} + \frac{1 - y}{1 - a}

  • dz=dL(a,y)dz\mathrm{d} z = \frac{\mathrm{d} L(a, y)}{\mathrm{d} z},这里可以通过链式法则将其变为dz=dL(a,y)dadadz\mathrm{d} z = \frac{\mathrm{d} L(a, y)}{\mathrm{d} a} \frac{\mathrm{d} a}{\mathrm{d} z}

    • Sigmoid

      • 根据微积分可以获得dadz=a(1a)\frac{\mathrm{d} a}{\mathrm{d} z} = a (1 - a)
      • 根据上一步可以获得dL(a,y)da=ya+1y1a\frac{\mathrm{d} L(a, y)}{\mathrm{d} a} = - \frac{y}{a} + \frac{1 - y}{1 - a}

      最终我们可以将这两个公式组合后化简为dz=ay\mathrm{d} z = a - y

    • ReLU的化简方式差不多,下面的代码会具体说明

  • dw=dL(a,y)dadadzdzdw=dzdzdw=dzx\mathrm{d} w = \frac{\mathrm{d} L(a, y)}{\mathrm{d} a} \frac{\mathrm{d} a}{\mathrm{d} z} \frac{\mathrm{d} z}{\mathrm{d} w} = \mathrm{d} z \cdot \frac{\mathrm{d} z}{\mathrm{d} w} = \mathrm{d} z \cdot x,其中xx是指对应FP中的输入项a[l1]a^{[l - 1]}

  • 同理可得db=dz\mathrm{d} b = \mathrm{d} z


在向量化的过程dZ=AY\mathrm{d}Z = A - Y很好理解,因为ZZAAYY是同维度的向量,但是dWdWdbdb的求解就不是那么容易了。首先从求解dWdW入手,我们有m个变量所以如果不使用向量化过程应该是这样的:

样本序号 dW\mathrm{d}W演算过程 db\mathrm{d}b演算过程
NaN dW=0\mathrm{d}W = 0 db=0\mathrm{d}b = 0
1 dW+=x(1)dz(1)\mathrm{d}W += x^{(1)} \mathrm{d}z^{(1)} db+=dz(1)\mathrm{d}b += \mathrm{d}z^{(1)}
2 dW+=x(2)dz(2)\mathrm{d}W += x^{(2)} \mathrm{d}z^{(2)} db+=dz(2)\mathrm{d}b += \mathrm{d}z^{(2)}
... ... ...
m dW+=x(m)dz(m)\mathrm{d}W += x^{(m)} \mathrm{d}z^{(m)} db+=dz(m)\mathrm{d}b += \mathrm{d}z^{(m)}
求均值 dW=dW/m\mathrm{d}W = \mathrm{d}W / m db=db/m\mathrm{d}b = \mathrm{d}b / m

可以看到如果我们使用原始方式需要有显式的for循环,我们可以使用向量化来避免for循环。这里先给出求上述变量的代码:

dW = 1. / m * np.dot(dZ, A_prev.T)
db = 1. / m * np.sum(dZ, axis=1, keepdims=True)

主要步骤

  1. dz[l]=da[l]g[l](z[l])\mathrm{d} z^{[l]} = \mathrm{d} a^{[l]} * g^{[l]'}(z^{[l]}),需要注意的是:
    • 这里*是乘以对应的每个元素,而不是点乘
    • g[l](x)g^{[l]}(x)是一个层的AF(e.g. Sigmiod函数),这里使用的是AF的导数
  2. dW[l]=dz[l]a[l1]\mathrm{d} W^{[l]} = \mathrm{d} z^{[l]} \cdot a^{[l - 1]}
  3. db[l]=dz[l]\mathrm{d} b^{[l]} = \mathrm{d} z^{[l]}
  4. da[l1]=W[l]Tdz[l]\mathrm{d} a^{[l - 1]} = W^{[l]^{T}} \cdot \mathrm{d} z^{[l]},这一步也是可以使用链式求导获得

DNN的BP中关键函数及其说明

# 第一个dAL的计算过程
# dAL和AL的shape为[m, 1] m为传入的样本数量
dAL = -(np.divide(Y, AL) - np.divide(1 - Y, 1 - AL))


def linear_activation_backward(dA, cache, activation):
    """
    反向传播
    -------
    Arguements:
    dA -- 使用激活函数后的值的导数,shape为[nl, m],nl为l层的神经元数量,m为样本数量
    cache -- 也就是Z,其shape与dA相同
    activation -- 激活函数
    """
    linear_cache, activation_cache = cache

    # 根据activation function不同计算不同dZ
    if activation == "relu":
        dZ = relu_backward(dA, activation_cache)
    elif activation == "sigmoid":
        dZ = sigmoid_backward(dA, activation_cache)

    dA_prev, dW, db = linear_backward(dZ, linear_cache)

    return dA_prev, dW, db


def linear_backward(dZ, cache):
    """
    根据dZ, 上一层A,W和b计算dW, db和上一层的dA
    -------
    Arguements:
    dZ -- 是linear_activation_backward()计算得到的dZ
    cache -- A_prev, W和b分别是上一层的A,本层的W和b
    """
    A_prev, W, b = cache
    m = A_prev.shape[1]

    # 后面的除以m是因为在向量化的情况下Loss Func改变了
    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


def relu_backward(dA, cache):
	"""
	ReLU反向传播(加入向量化)
	-------
	Arguements:
	dA -- 也就是dAL
	cache -- Z
	"""
    Z = cache
    dZ = np.array(dA, copy=True) # just converting dz to a correct object.
    
    # 如果z<=0就将其设置为0
    # Z = [-1.23, 2, 3] -> dZ = [0, 2, 3]
    dZ[Z <= 0] = 0
    
    assert (dZ.shape == Z.shape)
    
    return dZ


def sigmoid_backward(dA, cache):
    """
        Sigmoid反向传播(加入向量化)
        -------
        Arguements:
        dA -- 也就是dAL
        cache -- Z
	"""
    Z = cache
    
    s = 1/(1+np.exp(-Z))
    # Q: 为什么不用A - Y
    # A: 因为那只能应用于最后一层的AF不具有普遍性
    dZ = dA * s * (1-s)
    
    assert (dZ.shape == Z.shape)
    
    return dZ

Vectorized Implementation - 向量化实现

向量化用来消除显式的for循环,Andrew Ng做了一个实现,发现for循环会显著的慢于向量化的实现,尤其是在数据量很大的情况下,向量化的优化效果将会更加明显。

那么就说一说怎么实现的。在Logistic Expression中,z[l]=W[l]a[l1]+b[l]z^{[l]} = W^{[l]} a^{[l-1]} + b^{[l]}(其中a[0]=xa^{[0]} = x)是最核心的思想,各个变量的维度在<Error Back Propagation - Forward Propagation - 主要步骤 - 1>中做了详细的描述,这个公式仅用于仅有一个样本,如果有m个样本时正常的思路是使用for循环遍历m遍,但是如上面所说for循环会使得效率降低,所以这时候把样本堆叠起来形成一个向量:

Z[l]=W[l]A[l1]+b[l]Z^{[l]} = W^{[l]} \cdot A^{[l-1]} + b^{[l]}

  1. Z[l]Z^{[l]}是m个z[l]z^{[l]}堆叠后的结果,它的结构为(n[l],m)(n^{[l]}, m),也就是$ \begin{bmatrix} z^{l(0)} & z^{l(1)} & z^{l(2)} \cdots & z^{l(m-1)} \end{bmatrix}$
  2. W[l]W^{[l]}不变;
  3. A[l1]A^{[l-1]}同1,只不过他是堆叠了m个a[l1]a^{[l-1]},它的结构为(n[l1],m)(n^{[l-1]}, m)
  4. b[l]b^{[l]}的维度可以认为它不变,但是这其实在python中通过broadcast机制将其扩展为(n[l],m)(n^{[l]}, m)
  5. 任何导数(d\mathrm{d}*,如dZ[l]\mathrm{d} Z^{[l]})将于原矩阵结构一致。

REFERENCES