deeplearning.ai 第一部分:神经网络与深度学习

本篇博客为 deeplearning.ai 第一部分的学习笔记。

深度学习引言

什么是神经网络?

神经网络就是由若干神经元组合而成的网络结构,其包含输入层隐藏层输出层。而含有多层隐藏层的神经网络即为深度神经网络。下图给出了一个深度神经网络的示意图。

神经网络的监督学习

在深度学习领域,目前为止几乎所有的经济价值都是由基于监督学习的神经网络所创造的。

对于不同的应用领域,我们需要不同类型的神经网络:

  • 对于房价预测和在线广告等应用,采用的是相对标准的神经网络
  • 对于图像领域的应用,常常使用卷积神经网络(CNN
  • 对于序列数据,例如音频、语言等,常常使用循环神经网络(RNN)(注意不要与递归神经网络混淆)

监督学习所处理的数据可以分为结构化非结构化两种:

  • 结构化数据:数据以类似关系型数据库的表结构的形式展示
  • 非结构化数据:音频、图像或文本等特征无法直接展示的数据

为什么深度学习会兴起?

深度学习兴起的原因主要有三点: + 信息化社会带来的数据量的巨大提升 + 硬件更新带来更快的计算速度 + 神经网络算法的不断发展

思维导图

神经网络的编程基础

经典的神经网络可以理解为逻辑回归的叠加。本节将介绍逻辑回归的基本原理及其程序实现。

符号定义

  • \(x\):表示输入,维度为 \((n_x,1)\)
  • \(y\):表示输出,取值为 \((0,1)\)
  • \((x^{(i)},y^{(i)})\):表示第 \(i\) 组数据
  • \(m\):表示训练集的样本个数
  • \(m_{test}\):表示测试集的样本个数
  • \(X = [x^{(1)}, x^{(2)}, \ldots, x^{(m)}]\):表示所有训练数据集的输入值,维度为 \((n_x,m)\)
  • \(Y = [y^{(1)}, y^{(2)}, \ldots, y^{(m)}]\):表示所有训练数据集的输出值,维度为 \((1,m)\)

逻辑回归

逻辑回归适用于二分类问题。其公式为: \[ \hat{y}=P(y=1\mid x), where\;0\le\hat{y}\le1 \]

逻辑回归中具体的参数包括:

  • 输入特征向量:\(x \in \mathbb{R}^{n_x}\)

  • 训练标签:\(y \in \{0,1\}\)

  • 权重:\(w \in \mathbb{R}^{n_x}\)

  • 输出:\(\hat{y} = \sigma(w^Tx+b)\)

  • sigmoid函数:\(s = \sigma(w^Tx+b) = \sigma(z) = \frac 1 {1+e^{-z}}\)

逻辑回归的代价函数

为了训练参数 \(w\)\(b\),我们需要定义一个代价函数。而线性回归采用的最小二乘函数会导致局部最优(非凸优化),并不适用于逻辑回归。这里选择如下的损失函数(交叉熵函数),该损失函数本质上是由极大似然估计得出的。

\[ L(\hat{y^{(i)}},y^{(i)}) = -(y^{(i)}\log(\hat{y}^{(i)})+(1-y^{(i)})\log(1-\hat{y}^{(i)})) \]

因此代价函数为: \[ J(w,b) = \frac 1 m \sum_{i=1}^m L(\hat{y}^{(i)},y^{(i)}) = - \frac 1 m \sum_{i=1}^m[y^{(i)}\log(\hat{y}^{(i)})+(1-y^{(i)})\log(1-\hat{y}^{(i)})] \]

需要注意在命名上,损失函数通常指计算单个训练样本的误差,而代价函数则是整个训练集损失函数的平均。

梯度下降

得到了代价函数后,我们需要一种方法来找出最小化代价函数的 \(w\)\(b\)。容易证明代价函数是凸函数(能够找出全局最优),因此这里采用梯度下降法,不断沿着梯度方向更新参数,直至收敛

\[ w = w - \alpha * \frac {d(J(w,b) )} {dw} \\ b = b - \alpha * \frac {d(J(w,b))} {db} \]

公式中的 \(\alpha\) 称为学习速率,控制梯度更新的步幅。在逻辑回归中,一般初始化参数为 0。

逻辑回归中的梯度下降

在逻辑回归中,梯度下降涉及到复合求导,需要基于链式法则求解。课程中介绍了计算图的方法,能够更加直观地求解复合导数,而这其实可以看做一种简单的反向传播。

下面给出求解含有 m 个样本的逻辑回归的梯度下降的伪代码

变量名如下:

X1                  Feature
X2 Feature
W1 Weight of the first feature.
W2 Weight of the second feature.
B Logistic Regression parameter.
M Number of training examples
Y(i) Expected output of i

基于复合求导得出的导数如下:

d(a)  = d(l)/d(a) = -(y/a) + ((1-y)/(1-a))
d(z) = d(l)/d(z) = a - y
d(W1) = X1 * d(z)
d(W2) = X2 * d(z)
d(B) = d(z)

伪代码如下:

J = 0; dW1 = 0; dW2 =0; dB = 0;                 # Devs
W1 = 0; W2 = 0; B=0; # Weights
for i = 1 to m
# Forward pass
z(i) = W1*X1(i) + W2*X2(i) + b
a(i) = Sigmoid(z(i))
J += (Y(i)*log(a(i)) + (1-Y(i))*log(1-a(i)))

# Backward pass
dz(i) = a(i) - Y(i)
dW1 += dz(i) * X1(i)
dW2 += dz(i) * X2(i)
dB += dz(i)
J /= m
dW1/= m
dW2/= m
dB/= m

# Gradient descent
W1 = W1 - alpha * dW1
W2 = W2 - alpha * dW2
B = B - alpha * dB

上述伪代码实际上存在两组循环(迭代循环没有写出),会影响计算的效率,我们可以使用向量化来减少循环。

向量化

向量化可以避免循环,减少运算时间,Numpy 的函数库基本都是向量化版本。向量化可以在 CPU 或 GPU 上实现(通过 SIMD 操作),GPU 上速度会更快。

向量化逻辑回归

下面将仅使用一组循环来实现逻辑回归:

输入变量为:

X       Input Feature, X shape is [Nx,m]
Y Expect Output, Y shape is [Ny,m]
W Weight, W shape is [Nx,1]
b Parameter, b shape is [1,1]

向量化后的伪代码如下:

W = np.zeros((Nx, 1))
b = 0
dW = np.zeros((Nx, 1))
db = 0

for iter in range(1000):
Z = np.dot(W.T, X) + b # Vectorization, then broadcasting, Z shape is (1, m)
A = 1 / (1 + np.exp(-Z)) # Vectorization, A shape is (1, m)
dZ = A - Y # Vectorization, dZ shape is (1, m)
dW = np.dot(X, dZ.T) / m # Vectorization, dW shape is (Nx, 1)
db = np.sum(dZ) / m # Vectorization, db shape is (1, 1)

W = W - alpha * dW
b = b - alpha * db

Python/Numpy 使用笔记

下面介绍课程中提到的一些 python/numpy 的使用 tips。

Tip 1: 在 Numpy 中,obj.sum(axis = 0) 按列求和,obj.sum(axis = 1) 按行求和,默认将所有元素求和。

Tip 2: 在 Numpy 中,obj.reshape(1, 4) 将通过广播机制(broadcasting)重组矩阵 。reshape 操作的调用代价极低,可以放在任何位置。广播机制的原理参考下图:

Tip 3: 关于矩阵 shape 的问题:如果不指定一个矩阵的 shape,将生成 "rank 1 array",会导致其 shape 为 (m, ),无法进行转置。对于这种情况,需要进行 reshape。可以使用 assert(a.shape == (5,1)) 来判断矩阵的 shape 是否正确

Tip 4: 计算 Sigmoid 函数的导数:

s = sigmoid(x)
ds = s * (1 - s)

Tip 5: 如何将三维图片重组为一个向量:

v = image.reshape(image.shape[0]*image.shape[1]*image.shape[2],1)

Tip 6: 归一化输入矩阵后,梯度下降将收敛得更快。

构建神经网络

构建一个神经网络一般包含以下步骤:

  1. 定义神经网络的结构
  2. 初始化模型参数
  3. 重复以下循环直至收敛:
    • 计算当前的代价函数(前向传播)
    • 计算当前的梯度(反向传播)
    • 更新参数(梯度下降)

对于神经网络的训练,数据集的预处理与超参数(如学习速率)的调整十分重要。

思维导图

浅层神经网络

神经网络概述

与逻辑回归的对比

逻辑回归的结构如下:

X1  \  
X2 ==> z = XW + B ==> a = Sigmoid(z) ==> l(a,Y)
X3 /

而一个单层神经网络的结构如下:

X1  \  
X2 => z1 = XW1 + B1 => a1 = Sig(z1) => z2 = a1W2 + B2 => a2 = Sig(z2) => l(a2,Y)
X3 /

因此,我们可以将神经网络简单理解为逻辑回归的叠加。

表示与计算

本节将定义含有一层隐藏层的神经网络

  • a0 = x 表示输入层
  • a1 表示隐藏层的激活值
  • a2 表示输出层的激活值

计算神经网络的层数时,我们一般不考虑输入层(即本节讨论的是两层神经网络)。下图给出了一个神经网络的前向传播计算公式:

在该网络中,隐藏层的神经元数量(noOfHiddenNeurons)为 4,输入的维数(nx)为 3。计算中涉及到的各个变量及其大小如下:

  • W1 是隐藏层的参数矩阵, 其形状为 (noOfHiddenNeurons, nx)
  • b1 是隐藏层的参数矩阵, 其形状为 (noOfHiddenNeurons, 1)
  • z1z1 = W1*X + b 的计算结果,其形状为 (noOfHiddenNeurons, 1)
  • a1a1 = sigmoid(z1) 的计算结果,其形状为 (noOfHiddenNeurons, 1)
  • W2 是输出层的参数矩阵,其形状为 (1, noOfHiddenNeurons)
  • b2 是输出层的参数矩阵,其形状为 (1, 1)
  • z2z2 = W2*a1 + b 的计算结果,其形状为 (1, 1)
  • a2a2 = sigmoid(z2) 的计算结果,其形状为 (1, 1)

代码实现

两层神经网络前向传播的伪代码如下:

for i = 1 to m
z[1, i] = W1*x[i] + b1 # shape of z[1, i] is (noOfHiddenNeurons,1)
a[1, i] = sigmoid(z[1, i]) # shape of a[1, i] is (noOfHiddenNeurons,1)
z[2, i] = W2*a[1, i] + b2 # shape of z[2, i] is (1,1)
a[2, i] = sigmoid(z[2, i]) # shape of a[2, i] is (1,1)

如果对整个训练集进行向量化,得到新的 X 形状为 (Nx, m),则新的伪代码如下:

Z1 = W1X + b1     # shape of Z1 (noOfHiddenNeurons,m)
A1 = sigmoid(Z1) # shape of A1 (noOfHiddenNeurons,m)
Z2 = W2A1 + b2 # shape of Z2 is (1,m)
A2 = sigmoid(Z2) # shape of A2 is (1,m)

其中样本数量 m 始终表示列的维数,X 可以写为 A0

激活函数

常见激活函数

sigmoid

sigmoid 激活函数的取值范围是 [0,1] 。

注意 sigmoid 可能会导致梯度下降时更新速度较慢。其代码实现如下:

sigmoid = 1 / (1 + np.exp(-z)) # Where z is the input matrix

tanh

tanh 激活函数的取值范围是 [-1,1](sigmoid 函数的偏移版本)。

对隐藏层来说,tanh 比 sigmoid 的效果更好,因为其输出的平均值更接近0,这使得下一层数据更加靠近中心(便于梯度下降)。而 tanh 与 sigmoid 存在同样的缺点,即如果输入过大或过小,则斜率会趋近于0,导致梯度下降出现问题。

代码实现如下:

tanh = (np.exp(z) - np.exp(-z)) / (np.exp(z) + np.exp(-z)) # Where z is the input matrix
tanh = np.tanh(z) # Where z is the input matrix

ReLU

ReLU 函数可以解决梯度下降慢的问题(针对正数)。如果你的问题是二元分类(0或1),那么输出层使用 sigmoid,隐藏层使用 ReLU。

代码实现如下:

ReLU = np.maximum(0,z) # so if z is negative the slope is 0 and if z is positive the slope remains linear.

leaky ReLU

leaky RELU 与 ReLU 的区别在于当输入为负值时,斜率会较小(不为0)。它和 ReLU 同样有效,但大部分人使用 ReLU。

代码实现如下:

leaky_ReLU = np.maximum(0.01*z,z)  #the 0.01 can be a parameter for your algorithm.

目前激活函数的选择并没有普适性的准则,需要尝试各种激活函数(也可以参考前人的经验)

激活函数的非线性

线性激活函数会输出线性的激活值,因此无论你有多少层隐藏层,激活都将是线性的(类似逻辑回归),这会使隐藏层会失去意义,无法处理复杂的问题。因此我们需要非线性的激活函数。

注意当输出是实数时,可能需要使用线性激活函数,但即便如此如果输出非负,那么使用 ReLU 函数更加合理。

激活函数的导数

sigmoid 函数:

A = 1 / (1 + np.exp(-z))
dA = (1 / (1 + np.exp(-z))) * (1 - (1 / (1 + np.exp(-z))))
dA = A * (1 - A)

tanh 函数:

A = (np.exp(z) - np.exp(-z)) / (np.exp(z) + np.exp(-z))
dA = 1 - np.tanh(z)^2 = 1 - A^2

ReLU 函数:

A = np.maximum(0,z)
dA = { 0 if z < 0
1 if z >= 0 }

leaky ReLU 函数:

A = np.maximum(0.01*z,z)
dA = { 0 if z < 0
1 if z >= 0 }

神经网络的梯度下降

反向传播的公式与伪代码如下:

随机初始化

在逻辑回归中随机初始化权重并不重要,而在神经网络中我们需要进行随机初始化。

如果在神经网络中将所有权重初始化为0,那么神经网络将不能正常工作:所有隐藏层会完全同步变化(计算同一个函数),每次梯度下降迭代所有隐藏层会进行相同的更新。注意 bias 初始化为0是可以的

为了解决这个问题我们将 W 初始化为一个小的随机数:

W1 = np.random.randn((2,2)) * 0.01    # 0.01 to make it small enough
b1 = np.zeros((2,1)) # its ok to have b as zero

对于 sigmoid 或 tanh 来说,我们需要随机数较小,因为较大的值会导致在训练初期线性激活输出过大,从而使激活函数趋向饱和,导致学习速度下降。而如果没有使用 sigmoid 或 tanh 作为激活函数,就不会有很大影响。

常数 0.01 对单层隐藏层来说是合适的,但对于更深的神经网络来说,这个参数会发生改变来保证线性计算得出的值不会过大。

思维导图

深层神经网络

深层神经网络概述

深层神经网络是指隐藏层超过两层的神经网络:

符号定义

  • 我们使用 L 来定义神经网络的层数(不包含输入层)
  • n 表示每一层的神经元数量集合
    • n[0] 表示输入层的维数
    • n[L] 表示输出层的维数
  • g 表示每一层的激活函数
  • z 表示每一层的线性输出
    • Z 表示向量化后的线性输出
  • wb 表示每一层线性输出的对应参数
    • WB 表示向量化后的参数
  • a 表示每一层的激活输出
    • a[0] 表示输出,a[L] 表示输出
    • A 表示向量化后的激活输出

深层网络中的前向传播

对于单个输入,前向传播的伪代码如下:

z[l] = W[l]a[l-1] + b[l]
a[l] = g[l](z[l])

对于 m 个输入(向量化),前向传播的伪代码如下:

Z[l] = W[l]A[l-1] + B[l]
A[l] = g[l](Z[l])

我们无法对整个前向传播使用向量化,需要使用 for 循环(即每一层要分开计算)。

维数的确认

我们需要确保各个向量的维数能够匹配,这里用 l 表示当前是第几层。各向量的具体维数如下:

  • w[l]dw[l] 的维数:(n[l], n[l-1])
    • W[l]dW[l] 的维数: (n[l], n[l-1])
  • b[l]db[l] 的维数:(n[l], 1)
    • B[l]dB[l] 的维数: (n[l], m)
  • z[l]a[l] 的维数:(n[l], 1)
    • Z[l]A[l] 的维数:(n[l], m)
    • dZ[l]dA[l] 的维数:(n[l], m)

为什么要进行深层表示?

我们可以从两个角度解释为什么使用多个隐藏层:

  • 多个隐藏层可以将问题从简单到复杂进行拆分,先考虑简单的特征,再逐步变得复杂,最终实现预期的效果;
  • 电路理论表明越少的层数需要的单元数呈指数级上升,对神经网络来说也是如此。对于一个复杂任务来说,层数越少每一层所要包含的神经元数量会爆炸式增长。

深层神经网络的模块

深层神经网络一般包含前向传播反向传播两个模块:前向传播模块得到代价函数,后向传播模块计算各层参数的梯度,最后通过梯度下降来更新参数,进行学习。

在实际实现中,我们需要通过缓存将前向传播中的某些参数传递到反向传播中,帮助进行梯度的计算。

前向传播模块

向量化后的伪代码如下:

Input  A[l-1]
Z[l] = W[l]A[l-1] + B[l]
A[l] = g[l](Z[l])
Output A[l], cache(Z[l], W[l], B[l])

反向传播模块

向量化后的伪代码如下:

Input dA[l], Caches
dZ[l] = dA[l] * g'[l](Z[l])
dW[l] = (1/m) * np.dot(dZ[l], A[l-1].T)
dB[l] = (1/m) * np.sum(dZ[l], axis=1, keepdims=True)
dA[l-1] = np.dot(W[l].T, dZ[l])
Output dA[l-1], dW[l], dB[l]

最后一层 dA 的求解基于代价函数得出,注意计算时应去除 1/m 这一项,防止重复计算。

参数与超参数

在神经网络中,参数主要指 wb。而超参数指影响参数选择的参数,例如:

  • 学习速率
  • 迭代次数
  • 隐藏层层数
  • 隐藏层单元数
  • 激励函数的选择

深度学习是一个经验主义的过程,随着外界条件的不断变化,需要进行多次的实验来确定最佳的超参数与参数。

深层神经网络与大脑的关系

神经网络的单个逻辑单元与实际的神经元在结构上有一些相似,但大脑的工作原理目前还是未知的,所以无法进行进一步比较。

思维导图