TIP

DeepLearning, Andrew Ng 慕课笔记

# 1 Neural Networks and DeepLearning

# 1.1 Introduction to DeepLearning

# 1.1.1 Welcome

# 1.1.2 What is a Neural Network

# 1.1.3 Supervised Learning with Neural Networks

Structured Data : Database Unstructured Data : Image Audio Text

# 1.1.4 Why is Deep Learning taking off?

DeepLearning01

数据量的增加,使得深度学习的优势越来越明显

DeepLearning02

迭代过程需要的时间越长,所能实现的想法就越少,因此训练的算法改良对深度学习来说是一个很关键的问题。 比如使用 sigmoid 函数作为激活函数时,过大的输入会使神经网络饱和,梯度下降算法越来越慢;改良为 ReLU 函数(修正线性单元)后,梯度下降的算法运行的更快,迭代时间相应就减少了很多

# 1.1.5 About this Course

# 1.1.6 Course Resources

# 1.2 Basics of Neural Network programming

# 1.2.1 Binary Classification

  • 如果想要识别图片中是否有猫,可以将输入和输出组织成这样:输入为 64 * 64 * 3 维(像素点 64 * 64,三个颜色通道),输出为一个数字(0 或者 1)
  • 我们对输入和输出符号做以下约定:
    • nx 为输入的维度,比如这里就是 64 * 64 * 3
    • 用 X 将 x 按列组织起来
    • 用 Y 将 y 按行组织起来
    • 我们可以用 Python 里的X.shape()查看规模

# 1.2.2 Logistic Regression

  1. 相对于输入一个XX,输出yy代表这个图片是不是猫,我们更喜欢它能够输出这个图片是猫的概率,这里便用y^\hat{y}表示
  2. 我们可以使用y^=wT+b\hat{y} = w^T+b来表示这个关系,其中ww是特征权重,维度与特征向量相同,但是这个函数是关于xx的一个线性函数,这无疑是很糟糕的,因为我们想要的y^\hat{y}是一个在 0 到 1 之间的数呀
  3. 这个时候我们就会使用到sigmoid函数了,这个神奇的函数将整个实数域映射到了 0-1 之间,将线性映射为非线性,那么,继续吧

# 1.2.3 Logistic Regression Cost Function

DeepLearning03

  1. 计算误差时,如果使用误差的平方作为损失函数很有可能得到多个“低谷”(很多局部最优),使用梯度下降法将得不到真正最优的解
  2. 我们使用(ylogy^+(1y)log(1y^))-(ylog\hat{y}+(1-y)log(1-\hat{y}))作为损失函数,至于为什么可以使 y 分别等于 0 和 1 推算一下(y 也只有这么两个可取的值)
  3. 相对于只作用于某一样本的 loss function(损失函数),cost function(成本函数)J 是作用于整个样本集上的函数;对于某一样本,我们使用 loss function 来衡量 yyy^\hat{y}的差距,相对应的,对于某一样本集,我们使用 cost function 来衡量 W 和 b 的效果

# 1.2.4 Gradient Descent

  1. 因为我们选用了 logistic 的损失函数,所以我们将得到一个最优解而不是像平方误差函数那样的多个局部最优解,因此,我们随意地初始化这么一个 w 和 b,它最终都会随着梯度下降逐步收敛于我们想要的那个解
  2. 所谓梯度下降,就是在每一个状态下沿着下降最快的方向降低
  3. 现在先不考虑 b,只考虑 w,我们很容易根据 J 对 w 的导数得到使得 J 下降的 w 的方向,所以我们根据这个“方向”对 w 进行调整,就像:w=αdJ(w)dww -= \alpha \frac{dJ(w)}{dw},这里的α\alpha称作学习率(learning rate),就是每次更新的步长
  4. 扩展到 w 和 b 的情况,我们每次迭代就需要执行w=αJ(w,b)ww -= \alpha \frac{\partial J(w,b)}{\partial w}b=αJ(w,b)bb -= \alpha \frac{\partial J(w,b)}{\partial b}

# 1.2.5 Derivatives

# 1.2.6 More Derivative Examples

# 1.2.7 Computation Graph

前向传播

# 1.2.8 Derivatives with a Computation Graph

  1. 反向传播
  2. 链式法则
  3. 导数用 dvar

# 1.2.9 Logistic Regression Gradient Descent

  1. 首先把正向传播的公式列出来:
    1. z=wT+bz=w^T+b
    2. y^=a=σ(z)\hat{y}=a=\sigma (z)
    3. L(a,y)=(ylog(a)+(1y)log(1a))L(a,y)=-(ylog(a)+(1-y)log(1-a))
  2. 首先计算dada(a 对 J 的导数,dJ 省略掉),我们很容易求得ya+1y1a\frac{-y}{a}+\frac{1-y}{1-a}
  3. 之后计算dzdz,根据链式法则,我们先计算出dadz=a(1a)\frac{da}{dz}=a(1-a),再乘上刚刚算出来的dada,我们便可以得到dz=aydz=a-y,很神奇吧,最终居然这么简单
  4. 最后,我们的dw1=x1dz;dw2=x2dz;db=dzdw_1=x_1dz ;dw_2=x_2dz;db=dz,你没看错,就是这么简单

# 1.2.10 Gradient Descent on m Examples

  1. 现在我们把上面的一个样本的算法应用到整个数据集上,很明显的,我们需要一个 for 循环去遍历 m 个样本,对每个样本进行累加并求取平均值
  2. 像前面一样的,我们在每一层循环内先正向传播计算 J,之后反向传播求导计算各项的导数,这并没有什么不同。
  3. 值得注意的的是,虽然我们是在计算每一个样本的,但是 w 和 b 是对于整个样本集而言的,所以我们在这里将样本集里所有 w 加和、b 也一样.当然这里只考虑了一个特征的 w,如果有多个特征(n 个),那么我们就对每个 w 都这么做,最终会得到dw1dw_1~dwndw_n还有dbdb,诶?这不就是我们要求的吗?那么还剩最后一步了
  4. 所谓最后一步,不过是让 w 减去αdw\alpha dw、d 减去αdb\alpha db,完成梯度下降的任务而已
  5. 我们这里有两个 for 循环(m 和 n),我们知道,循环是很慢的,如果可以,我们会尽最大能力降低时间复杂度,以获得更高的效率,我们在前面提到过这对想法的实现来说是很重要的一件事,但是具体要怎么提高速度呢,我们继续看下一节——向量化

# 1.2.11 Vectorization

  1. 前面已经提到,如果我们直接使用 for 循环,那么速度会非常的慢,慢到什么程度呢?一个直观的测试告诉我们,它会比向量化慢上 300 倍!下面,就让我们使用向量化来取代这奇慢的循环吧
  2. 让我们来尝试:
import numpy as np
a = np.array([1, 2, 3, 4]) # 我们先试试怎么初始化一个数组
1
2
a = np.random.rand(1000000)
b = np.random.rand(1000000)
c = np.dot(a, b) # 这样就完成了点乘了???
1
2
3

# 1.2.12 More Examples of Vectorization

  1. 让我们再看几个例子:

    1. np.exp(v) # 对每个元素求exp
    2. np.log(v) # 对每个元素求log
    3. np.abs(v) # 对每个元素求abs
    4. np.maximum(v) # 求整个矩阵的最大值
    5. v**2 # 对每个元素求平方
    6. 1/v # 对每个元素求倒数
  2. 现在,我们试着去优化那个有着两个 for 循环的神经网络过程,我们先简单的优化掉 n 这个 for 循环:

dw = np.zeros((n-x, 1)) # 因为要用矩阵,就将原来对每个w初始化为0改成初始化这样的一个全零矩阵
# ...
for i in range(m):
    # ...
    dw += xi dzi
    db += dzi
dw /= m
1
2
3
4
5
6
7

很明显,我们已经把里面的 n 循环优化掉了,也就是,可以一次将所有特征看做整体 w,而不必去对每个特征分别计算

  1. 很轻松地,我们完成了对一个 for 循环的优化,接下来,让我们把所有的 for 循环都优化掉吧 φ(>ω<*)

# 1.2.13 Vectorizing Logistic Regression

  1. 在前面我们利用 X 将各组训练数据按列组织起来,我们现在再看如何利用矩阵对这些数据进行计算
  2. 我们先看一个训练集的时候,我们是使用z(i)=wTx(i)+bz^{(i)} = w^Tx^{(i)}+ba(i)=σ(z(i))a^{(i)}=\sigma(z^{(i)})对每个训练集的前向传播进行计算的
    1. 这里ww是(n, 1)的矩阵,wTw^T自然就是(1, n)的矩阵(也就是上节课说的将 n 个输入特征组织起来)
    2. 再看x(i)x^{(i)},这也是一组训练集的数据,对应于 n 个输入特征,很明显,它是(n, 1)的矩阵
    3. 权重和输入特征矩阵相乘,最后输出的是一个数字(或者说(1, 1)矩阵),我们用它加上 b,自然就是我们想要的值
    4. sigmoid 函数不必说也知道
  3. 我们接下来看如何对 m 个训练集同时处理
    1. wwwTw^T自然是不变的,他们不可能随着训练集的数量增加而增加,所以它仍然是(1, n)的矩阵
    2. 继续看 X,不必说也知道它是(n, m)的矩阵了,每一列都是刚刚的一个训练集
    3. 然后我们利用矩阵的乘法使两个矩阵相乘,如果我们还对矩阵乘法有点印象的话,就可以知道结果的 i 行 j 列元素对应的就是第一个矩阵的 i 行与第二个矩阵的 j 列相乘并求和,看到这里想必大家都明白了,前面的每一行(虽然现在只有这么一行)不正对应于一个训练集的所对应的权重ww嘛,后面的每一列不正对应于一个训练集的输入数据嘛,最后的结果不过是将原来的数据列在了一起,形成最后这样的(1, n)的矩阵,比如说(0, 3)是由特征权重 w 和第四组训练集相乘得来,也就是原来 for 循环的第四组
    4. 然后让我们加上 b 吧,既然每个数据都要加 b,那就做一个(1, n)的全 b 矩阵咯
    5. 然后 sigmoid 又不用说了,还是那么简单
  4. 下面我们用 Python 试一下
Z = np.dot(w.t, X) + b # 咦?居然直接这样就可以了?原来numpy会自动将它扩展为(1, n)的全b矩阵
A = σ(Z) # 对每个z求sigmoid
1
2

# 1.2.14 Vectorizing Logistic Regression's Gradient

  1. 上节课仅仅展示了对前向传播的简化示例,下面我们对反向传播进行简化
  2. 经过上节课的讲解,我们应该是知道了我们怎么用矩阵将一组训练集组织起来以及为何可以这么组织起来,这样,我们应该很容易将单个训练集的dz=aydz=a-y推到整个训练集的dZ=AYdZ=A-Y,他们都是(1, m)的矩阵
  3. 下面要推的是 dw 和 db,db 我们可以很容易的写出来db=1mnp.sum(dZ)db=\frac{1}{m}np.sum(dZ),dw 相应地,我们想得到那么一个(1, n)的矩阵代表待优化的 m 个特征,我们可以由每个训练集的dw=xdzdw=xdz推出在整个训练集上的dw=1mXdZTdw=\frac{1}{m}XdZ^T,哦,你会发现这会得到一个(n, 1)的矩阵,但是这没关系,反正我们要的是他们的和,也就是dw = np.sum(np.dot(X, dZ.T)) / m,嗯,我们不仅将算式推出来了,还将 Python 代码写出来了,完美,let's continue.
  4. 下面我们就是真正让 w 和 b 优化了,嗯,减一下就好,别忘了学习率
  5. 我们完美地、不显式使用一次循环地完成了一次迭代,哦,是整个训练集的一次迭代哦,如果我们多次迭代呢?我们暂时还没有简单的方法使他避免使用 for 循环,那就先使用 for 循环吧,就那样、简单地、在外面嵌套这么一层,就好了

# 1.2.15 Broadcasting in Python

  1. Python 的 numpy 会对某些不满足加减乘除条件的矩阵进行补全,比如某一维度为 1 的矩阵,这样使得很多操作会变得更加方便,这种特性就叫做广播

# 1.2.16 A note on python or numpy vectors

  1. 广播特性更多的是给我们带来了很大的灵活性,使我们写代码更加方便,但如果对广播特性并不了解的话,很容易出现我们不可预料的 bug
  2. 我们看np.random.randn(5),你可能会以为他是一个一维的向量,但是通过 print 它和它的转置可以发现,他不过是一个数组而已我们也可以通过观察它的括号层数来判断它的维度
  3. 使用上面说的数组很容易出现不可预料的错误,可能你会在使用的时候以为他是一个向量,所以就……那么如何避免呢?我们尽量总是使用np.random.randn(5, 1)或者np.random.randn(1, 5)这样的代码,另外使用几个 assert(a.sharp == (5, 1))以保证没有写错,还有我们要敢于使用 a.resharp(1, 5)以保证生成的是你想要形状的矩阵

# 1.2.17 Quick tour of Jupyter/iPython Notebooks

emmmmm,我选择暂时不安装,我还是更喜欢原生的 pyshell(idle)

# 1.2.18 Explanation of logistic regression cost function

  1. 我们使用y^\hat{y}来输入 x 时表示 y=1 的概率,那么,我们可以得出这样的一组式子:
    • p(yx)=y^p(y|x) = \hat{y} if y = 1
    • p(yx)=1y^p(y|x) = 1-\hat{y} if y = 0

哦,p(y|x)是什么鬼,让我们回忆下概率的知识,很容易猜出来这是在 x 的前提下 y 的概率,也就是刚刚说的输入 x 时 y 的概率,那就很明了了

  1. 我们将这两个式子写在一起p(yx)=y^y(1y^)(1y)p(y|x) = \hat{y}^y(1-\hat{y})^{(1-y)},我们不需要知道为什么这样写,我们只需要知道这样是对的,仔细看看,对吧?
  2. 因为我们的损失函数更多的是关注整体的求导之后的方向性(梯度下降最大的方向),我们使用 log 这个严格递增的函数加在刚推出来的概率上,得到log(p(yx))=ylogy^+(1y)log(1y^)log(p(y|x))=ylog\hat{y}+(1-y)log(1-\hat{y}),这个就是我们前面提到的损失函数的相反数L(y^,y)-L(\hat{y},y)。嗯?为什么带个负号?哦我们分别来看,前面我们的损失函数是什么呢,是计算预测值与实际输出的一个差,我们想要这个值尽量的小,我们再看看这里的这个,去掉 log 可是预测满足实际输出的概率啊,我们当然希望这个值尽量地大,那么这其中的逻辑与关系便可以解释地清楚了
  3. 刚刚我们推的是单样本的损失函数,这次我们求整个样本集的成本函数,根据最大似然估计法,我们已经出现的这个样本集所对应的概率应当是在该情况下出现概率最大的情况,我们将所有样本的概率乘在一起,然后一样地取 log,这样的话,你会发现每一项都是刚刚所说的损失函数的相反数,我们把负号拿出去,这里我们要的是它取最大值,就是要去掉负号的那个的最小值,这不正是我们想要的成本函数嘛……然后我们再进行适当的缩放,这并不影响它的结果的我们的成本函数便得到了:J(w,b)=1mi=1mL(y^(i),y(i))J(w,b)=\frac{1}{m}\sum_{i=1}^mL(\hat{y}^{(i)},y^{(i)})

# 1.3 Shallow neural networks

# 1.3.1 Neural Network Overview

  1. 我们已经学习了逻辑回归,现在我们要学习一下神经网络。至于神经网络,简单地说就是让我们上周学习的神经元排列成一个网络……天啊,一个神经元就要写那么多的代码,还要结个网???算了不学了不学了!等等,当然这张网是有规律组织起来的我们先看一下这张网长什么样子:

    DeepLearning04

  2. emmmmm,原来我们还只有一个圈圈,现在一下子变这么多了,不学了,别拦我(╯>д<)╯

  3. (偷眼瞧)貌似理解错了,我们原来做的貌似不止这么一个圈圈,我们原来的貌似是去掉了中间这四个圈圈的东西,现在的话,貌似只加上了这么一”层“而已,诶呀,这么说我就放心啦

  4. 多了一层会有什么后果呢?事实上我们在每个圈圈的位置都使用 w 和 b 对左侧输入进行变换,然后加上激活函数,最左面到中间的一层圈圈这样,中间一层圈圈到最右面一层(个)圈圈也是这样

  5. 那么中间这 4 个圈圈怎么计算呢,我们只做过 n 个输入到 1 个输出的情况呀。我们先只看一个圈圈,它经过 w 和 b 会得到一个值,后面几个圈圈也是这样呀,不过对他们处理的 w 和 b 是不一样的,我们使用下标进行区分,我们已经学过了怎么使用矩阵组织多维数据,那么这里我们可以很方便地将各个 w 组织起来

  6. 先讲到这里啦,不然下节课没得讲了(~ ̄ ▽  ̄)~

# 1.3.2 Neural Network Representation

  1. 现在讲讲层与层之间的关系,我们学过了相邻两层之间的关系,也就是说知道了 0-1 和 1-2 的方法,那么还需要知道更多吗?
  2. 哦,我只想说没了,有的话,就是记得上标(带中括号的小数字)代表层数,输入层一般不算作一层,或者叫第 0 层[0]^{[0]}
  3. 现在我们梳理一下各层的维度吧,以便更加直观地理解各层之间的关系:
  • xx(a[0]a^{[0]}) | (n[0],1)(n^{[0]}, 1) eg. (3, 1)
    • W[1]W^{[1]} | (n[1],n[0])(n^{[1]}, n^{[0]}) eg. (4, 3)
    • b[1]b^{[1]} | (n[1],1)(n^{[1]}, 1) eg. (4, 1)
  • a[1]a^{[1]} | (n[1],1)(n^{[1]}, 1) eg. (4, 1)
    • W[2]W^{[2]} | (1,n[1])(1, n^{[1]}) eg. (1, 4)
    • b[2]b^{[2]} | (1,1)(1, 1) eg. (1, 1)
  • y^\hat{y}(a[2]a^{[2]}) | (1,1)(1, 1) eg. (1, 1)

大概的看一下,我们可以发现,现在满足a[i]=σ(Wa[i1]+b[i])a^{[i]}=\sigma(Wa^{[i-1]}+b^{[i]}),而原来每个式子应该是满足a[i]=σ(wTa[i1]+b[i])a^{[i]}=\sigma(w^Ta^{[i-1]}+b^{[i]}),貌似差了一个转置,为啥呢?下节就可以知道了

# 1.3.3 Computing a Neural Network's output

  1. 嗯,我们先将我们以前的式子列出来z[i]=wTa[i1]+b[i]z^{[i]}=w^Ta^{[i-1]}+b^{[i]},不要管 i,现在只看某两层之间的关系,那么我们可以通过加下标的方式用以区分各个结点,我们很容易写出来这样的n[i]n^{[i]}个式子,我们将它按行组织起来,就形成了W[i]W^{[i]}了呀,哦上节里面的转置便是差在了这里,W 本来就是将 w 的转置组织起来的,自然不需要再转置。
  2. 很明显,我们已经在上节把需要推的推出来了,不过当前仅仅针对一次训练数据,我们再看看如何组织数据使得一次可以支持整个训练集的训练

# 1.3.4 Vectorizing across multiple examples

  1. 和之前一样的,我们用列组织训练集的数据,每一列是一次训练数据,我们在上标上加一个带括号的数字来表示这是第几次训练,比如(3)^{(3)}代表第三次训练
  2. 需要我们组织的有哪些数据呢,首先就是输入的 x,它会被组织成 X,相应地,输出项 y 会被组织成 Y,哦,我们还有考虑中间项,比如 z、a,事实上我们只有 z 和 a,x 和 y 均可以用 a 来表示
  3. 我们再看看刚刚我们组织的 A 和 Z,他们各个位置上分别代表什么呢?
    • 从左到右是按列组织的训练集各项,他们用右上角(i)^{(i)}来区分
    • 从上到下是各个特征的输入或输出,相关于特征个数 n,他们用右上角[i]^{[i]}来区分

# 1.3.5 Justification for vectorized implementation

  1. 前面我们已经在每一节课都认真地分析了总的训练集数据如何组织与为何这样组织,所以这节课可以说是非常轻松了
  2. 我们将wTw^T按行组织成一个矩阵,xx等训练数据按列组织成一个矩阵,至于为什么,我们应该已经很清楚了,WW中的一行对应于一个输出结点,与XX中的一列相乘自然就得到了一个输出结点的一次输出数据,我们将此行和此列横向平移和纵向平移,那么我们得到的数据自然就组成了一个矩阵,这个矩阵各个位置分别是啥,前面已经说过
  3. 最后我们再通过 Python 的广播机制加一个 b,就完成了

# 1.3.6 Activation functions

  1. 我们之前只用过 sigmoid 函数,但是事实上 sigmoid 函数很少使用的,相对来说 tanh 函数几乎总是比它表现的好,虽然 tanh 只是 sigmoid 向下平移变换就可以得来,但是 tanh 平均值是 0 而不是 0.5,这使得数据可以直接中心化
  2. 但是有一个例外情况,就是二分类,它最终需要一个 0~1 的值,而 sigmoid 函数本身就是 0~1 的值,所以我们可以用它作为输出层激活函数,而隐藏层往往使用别的激活函数
  3. 另外我们之前有介绍过 ReLu 函数,这是一个很好的激活函数,我们更多情况都是使用这个激活函数
  4. 除 ReLu 函数还有一个 Leaky ReLu 函数,不过它并不是特别的常用

# 1.3.7 why need a nonlinear activation function?

为什么需要这么一个非线性的激活函数呢?课上已经说得很明白了,如果使用线性激活函数那么多层神经网络都可以看成一层神经网络了,所以我们不会在隐层上使用线性激活函数,不过输出层的话是可以考虑的

# 1.3.8 Derivatives of activation functions

要用梯度下降法算反向传播的话,就一定要计算激活函数的导数咯

  1. sigmoid:a(1a)a(1-a)
  2. tanh:1a21-a^2
  3. ReLU:就是简单的分段函数,因为 0 处不可微,所以可以归到大于 0 或者小于 0 里面

# 1.3.9 Gradient descent for neural networks

propagation:

  1. Z[1]=W[1]x+b[1]Z^{[1]}=W^{[1]}x+b^{[1]}
  2. A[1]=σ(Z[1])A^{[1]}=\sigma(Z^{[1]})
  3. Z[2]=W[2]A[1]+b[2]Z^{[2]}=W^{[2]}A^{[1]}+b^{[2]}
  4. A[2]=σ(Z[2])A^{[2]}=\sigma(Z^{[2]})

back propagation:

  1. dZ[2]=A[2]YdZ^{[2]}=A^{[2]}-Y
  2. dW[2]=1mdZ[2]A[1]TdW^{[2]}=\frac{1}{m}dZ^{[2]}A^{[1]T}
  3. db[2]=1mnp.sum(dZ[2],axis=1,keepdims=True)db^{[2]}=\frac{1}{m}np.sum(dZ^{[2]},axis=1,keepdims=True)
  4. dZ[1]=W[2]TdZ[2]g[1](Z[1])dZ^{[1]}=W^{[2]T}dZ^{[2]}*g^{[1]'}(Z^{[1]}) # 这里是逐元素相乘
  5. dW[1]=1mdZ[1]XTdW^{[1]}=\frac{1}{m}dZ^{[1]}X^{T}
  6. db[1]=1mnp.sum(dZ[1],axis=1,keepdims=True)db^{[1]}=\frac{1}{m}np.sum(dZ^{[1]},axis=1,keepdims=True)

这里先把我们需要的公式都列出来了,至于为什么,下节课再说(事实上我们每节课都有思索过这些问题,大致上已经知道了),有几点注意的如下:

  1. axis=1 表示水平相加求和,db 自然是使用水平相加将所有样本数据加和
  2. keepdims 是防止 Python 输出那些古怪的秩数为 1 的矩阵
  3. 为了防止 Python 输出古怪的矩阵,可以显示调用 reshape

# 1.3.10 Backpropagation intuition

没什么,有几个值得注意的地方

  1. 反向传播公式 2 和 5 使用的是 X 的转置,那是因为我们当初一层时候使用的是dw=1mXdzTdw=\frac{1}{m}Xdz^T现在的 W 是 dw 的转置组织起来的,自然要转置咯
  2. 反向传播的时候我们可以通过增加对维度的判断(assert 等),以消除 bug
  3. 输出层的梯度是用梯度下降法计算得来的,之后的每层都是根据 W 分配得来

# 1.3.11 Random+Initialization

  1. 如果使用全零初始化权重,那么每次反向传播的值将会均匀的分配到每个隐藏单元对应的权重上,再多的隐藏单元也只相当于一个隐藏单元
  2. 我们可以使用随机初始化的权重以保证不会出现上面的情况,另外 b 是可以初始化为 0 的,因为梯度的分配与 W 有关,随机的 W 便可使 b 每次反向传播得到的不相同
  3. 我们可以这么初始化权重:w = np.random.rand(2, 2) * 0.01,咦,为什么有这么个 0.01,哦,原来是防止太大的初始值使得像 tanh 和 sigmoid 激活函数饱和,梯度太小

# 1.4 Deep Neural Networks

# 1.4.1 Deep L-layer neural network

  1. 我们用 ll 表示层数索引
  2. 其他的前面提到过,没啥可说的(我发现 1.3.2 那次整理真的很有必要啊,整理之后后面基本都是浏览而过了)

# 1.4.2 Forward and backward propagation

我们很容易得到前向传播和反向传播的关系式,或者说,我们已经在 1.3.9 写的很明白了这里 copy 一下吧: propagation:

  1. Z[l]=W[l]A[l1]+b[1]Z^{[l]}=W^{[l]}·A^{[l-1]}+b^{[1]}
  2. A[1]=g[l](Z[1])A^{[1]}=g^{[l]}(Z^{[1]})

back propagation:

  1. dZ[l]=W[l]TdZ[l]g[l](Z[l])dZ^{[l]}=W^{[l]T}dZ^{[l]}*g^{[l]'}(Z^{[l]}) # 这里是逐元素相乘
  2. dW[l]=1mdZ[l]A[l1]TdW^{[l]}=\frac{1}{m}dZ^{[l]}A^{[l-1]T}
  3. db[l]=1mnp.sum(dZ[l],axis=1,keepdims=True)db^{[l]}=\frac{1}{m}np.sum(dZ^{[l]},axis=1,keepdims=True)
  4. dA[l1]=W[l]TdZ[l]dA^{[l-1]}=W^{[l]T}dZ^{[l]}
  • 前向传播的时候可以将 Z 和 A 缓存起来以便后向传播使用

上周作业已经将这个实现……(因为参考链接根本不告诉我作业题里哪些是要自己写的,哪些是直接给出的,我又最讨厌 copy 源代码,只好边看边加功能)

# 1.4.3 Forward propagation in a Deep Network

emmmm 貌似没啥,到时候用 for 循环组织各层就好

# 1.4.4 Getting your matrix dimensions right

维度:

  1. W:(n[l],n[l1])W:(n^{[l]}, n^{[l-1]})
  2. b:(n[l],1)b:(n^{[l]}, 1) # Python 广播可以自动扩展
  3. ZA:(n[l],m)Z A:(n^{[l]}, m)

# 1.4.5 Why deep representations?

  1. 深度神经网络是一个从具体到抽象的过程
  2. 用人脸识别举例:比如说我们第零层是输入的特征,经过一层 W 的变换后自然得到了一些基本的线这样微小的,而且线性的特征,然后经过一层激活函数,继续传递到下一层,很明显,这一层可以得到上一层的不同组合了,可能我们得到了曲线,当层数不断提高,它所描述的特征越来越复杂,可能某一层已经足够描述某一器官比如鼻子的形状,某一层已经能表征出人脸的大概形状……
  3. 用声音识别举例:还是一样,最开始可能只能识别声调高低,之后某层可能识别音位,再之后某层可能已经可以识别单词,最后直到完整的句子
  4. 另外至于为什么层数多之后可以很有效地解决某些问题,可以参考电路理论,如果实现同样功能的神经网络,隐藏单元的数量可能成指数倍增长,反过来说就是,很多情况下,深层神经网络会比浅层神经网络好的多

# 1.4.6 Building blocks of deep neural networks

我们从yyy^\hat{y}得到 cost 等信息,然后利用 cost function 的导数计算反向传播梯度下降的的值,也就是dA[l]dA^{[l]},之后,根据前面 1.4.2 的公式就好,另外dA[0]dA^{[0]}算不算都可以,是个没用的值

# 1.4.7 Parameters vs Hyperparameters

  1. 超参数就是那些控制了我们最终需要的 W、b 的参数,比如学习率、梯度下降法循环的数量、隐藏层的数目、隐藏层单元数目、激活函数的选择,之后我们还会遇到更多
  2. 如何选择呢?我们要不断尝试与观察,看最后得到的 J 是否有下降,如此迭代以找到最合适的超参数
  3. 另外一个很重要的就是直觉,这当然是要反复尝试锻炼的啦

# 1.4.8 What does this have to do with the brain?

关系?也许当初是模拟神经元所做的吧,但是神经元绝对比这复杂多了,也许这样真的可以模拟出来大脑,也许根本不一样。总之,深度学习可以达到大脑的效果绝对是夸大的说法,至少,现在是这样的

# 2 Improving Deep Neural Networks

# 2.1 Practical aspects of Deep Learning

# 2.1.1 Train / Dev / Test sets

  1. 超参数的优化是一个反复迭代的过程,即便一个领域的人才,对这个领域有着丰富的经验,到了另外一个领域仍然需要这个过程
  2. 事实上,这个优化过程更取决于数据集、计算机配置等因素
  3. 我们大多数情况下将数据分为三部分
    • 训练集(training set) 用于训练
    • 验证集(dev set) 根据训练集得到的模型在验证集上的表现调优超参数,直至在该验证集验证下误差达到最小
    • 测试集(test set) 用于评估最终确定的模型的精确度(泛化能力)

TIP

为什么要分成这么三份?我们来考虑下以下几种情况:

  1. 一整个 training set ,很明显不可取,因为这样的话可能最终模型只拟合了 training set 这些数据
  2. training set + test set 由于超参数是用 test set 拟合的,所以很可能会出现对 test set 过拟合的现象
  3. 而再用一组在训练过程中从未“触碰”过的数据来验证就可以更好地证明模型的正确性

另外,可以对 training set 进行分组,每次取一部分作为 dev set ,这叫做交叉验证,尽管这是一个很好的方法,但是由于训练往往是很耗时的,所以这种方法并不常用

还有一点一定要注意的是,收集数据后尽量将它打乱,如果说将最开始收集到的数据作为 training set ,后收集到的数据作为 test set 的话,很明显这并不满足同分布

  1. 小数据量的情况下需要分配很大比例的数据用于测试,但是在当今的大数据时代,这样的做法属实是没太大的必要了,我们完全可以拿出更小的部分用以验证
  2. 训练集和验证集要确保来自同一分布
  3. 测试集并不是必要的,因为它只是对最终所选定的神经网络做出无偏评估

# 2.1.2 Bias /Variance

  1. 偏差?方差?有区别吗?如果就数学概念而言的话,感觉这两个提不到一块去,不过也确实,他们一个评估的是预测模型,一个评估的是样本数据,但是在 ML 这里我们会经常见到他们的

  2. 让我们看下偏差方差与我们训练模型有着什么样的关系:

    DeepLearning05

  3. 我们再用举一个简单的例子,这次就和实际操作有点联系了:

Train Set Error Dev Set Error
1% 11% 高偏差
15% 16% 高偏差
15% 30% 高偏差+高方差
0.5% 1% 低偏差+低方差
  1. 不过偏差与方差也是相对的,前面的假设是基于我们判断的误差接近于 0%的前提下而言的,这里称为最优误差或者贝叶斯误差。但如果,贝叶斯误差比较高,比如说 15%,此时再看上面的第二个模型,可以说方差与偏差都不高,算是比较合理的了

# 2.1.3 Basic Recipe for Machine Learning

那么有误差和方差的时候怎么调整呢?

  1. 先看偏差高不?高的话试试选择新的网络,比如增加更多隐藏层或者隐藏单元的网络,或者花费更多时间来训练网络
  2. 当偏差降低到可以接受的数值时,我们利用验证集检查一下方差是否有问题,如果方差较高,最好的解决方案就是采用更多数据,当然这有点难度,因此我们也可以通过尝试正则化来减少过拟合,如果可以找到合适的神经网络框架,有时是可以一箭双雕,同时减少方差和误差的,不过这个过程没有什么捷径了,就是反复的重复尝试……

# 2.1.4 Regularization

好难 QAQ

  1. 想解决过拟合问题嘛,大概就需要准备更多数据或者正则化了,但是我们往往没有获取更多的数据的条件,所以我们可以尝试使用正则化避免过拟合或减少网络误差
  2. 我们这里在成本函数 J 上增加一个正则化参数λ\lambda,因为 b 的维度较低,我们完全可以不对它进行正则化
  3. 我们通常使用 L2 正则化,它是λ2m\frac{\lambda}{2m}乘上ww范数的平方,也就是每个元素平方之和,可以表示为wTww^Tw
  4. 而 L1 正则化则是使用λm\frac{\lambda}{m}乘上j=1nxw\sum_{j=1}^{n_x}|w|,这样得到的ww将会是系数的,也就是其中会有很多 0,也许这会节省些内存吧,但是这并不是我们所关注的,所以我们没必要因此去使用它
  5. lambdalambda是 Python 中的一个保留字,我么可以使用lambdlambd来代替它
  6. 说这么多,怎么实现呢?我们先写下现在的成本函数:J(w[1],b[1],...,w[2],b[2])=1mi=1nL(y^(i),y(i))+λml=1LW[l]F2J(w^{[1]},b^{[1]},...,w^{[2]},b^{[2]})=\frac{1}{m}\sum_{i=1}^{n}L(\hat{y}^{(i)},y^{(i)})+\frac{\lambda}{m}\sum_{l=1}^L||W^{[l]}||_F^2,该矩阵范数被称作“弗罗贝尼乌斯范数”,用下标 F 标注,嗯,先这么叫好了,那么如何实现梯度下降???嗯?这个问题应该问吗?求导呗!
  7. dw[l]=backprop+λmw[l]dw^{[l]}=backprop+\frac{\lambda}{m}w^{[l]},哦,这里的backpropbackprop是原来没有正则化的dw[l]dw^{[l]},然后我们就可以进行梯度递降了
  8. w[l]=w[l]αdw[l]=w[l]αλmw[l]αbackprop=(1αλm)w[l]αbackpropw^{[l]} = w^{[l]} - \alpha dw^{[l]} = w^{[l]}-\frac{\alpha\lambda}{m}w^{[l]}-\alpha backprop = (1-\frac{\alpha\lambda}{m})w^{[l]}-\alpha backprop,这样我们就在w[l]w^{[l]}前加了一个小于 1 的系数,也就是趋向于使w[l]w^{[l]}更小,因此,L2 正则化也被称为“权重递减”,但是权重减不减什么的并不是我们所关注的,所以我们不必记住这个名字

TIP

正则化使得对模型做的任何事情都尽量减轻模型的复杂度,而不是试图去拟合数据

# 2.1.5 Why regularization reduces overfitting?

  1. 我们使用 L2 正则化的时候会有参数λ\lambda,很明显这个参数越大的话,参数 W 会越来越接近于 0,不过这有什么影响呢?我们直观的感受下

  2. 这里以激活函数为 tanh 的情况进行举例:

    DeepLearning06

    我们知道z[l]=W[l]a[l1]+b[l]z^{[l]}=W^{[l]}a^{[l-1]}+b^{[l]},那么也就是说W[l]W^{[l]}很小的时候,z[l]z^{[l]}也会很小,那么再计算a[l]a^{[l]}时,我们要通过这么一个激活函数对吧,我们可以看到,在激活函数输入很小的时候,它是线性的,而线性的意味着什么呢?再多的隐藏层都可以看做一层,也就是,简化了模型,这就使得模型很可能处于高偏差的状态,然后我们通过减小λ\lambda,就可以调整到那么一个,合适的、Just right 状态

  3. 应用梯度下降的时候一定要记得加上正则化项,这样才能保证梯度下降,否则我们可能只能使得原来的梯度下降,并没有使新的梯度函数下降

# 2.1.6 Dropout Regularization

下面介绍下一种正则化——Dropout(随机失活),这个理解起来相对于 L2 正则化简单得多

  1. 我们每次随机地让一部分结点失去作用,比如说以 0.5 的概率,这样会使得我们的神经网络变的更加的简单、规模更小,然后我们用 backprop 方法进行训练
  2. 那么如何实现呢?我们最常用的方法是 inverted dropout(反向随机失活),我们使用一个三层(l=3)的网络来举例,我们定义下第三层的 dropout 向量d[3]d^{[3]}d3 = np.random.rand(a3.shape[0], a3.shape[1]) < keep_prob 然后我们看下它时候小于某数,我们称之为 keep-prob,比如 0.5?0.9?它表示保留某个隐藏单元的概率,哦 Python 会把这个变成一个只有 True 和 False 的矩阵,然后怎么用呢?
  3. 下面,我们只需要对 a3 进行一点小小的改变
a3 = np.multiply(a3, d3) # a3 *= d3`
a3 /= keep_prob
1
2

完了

  1. 上面第一步是让结点随机失活,第二步是让期望值保持原来的水平(否则会损失一部分的)
  2. 不过我们测试的时候可不能加上 d 的影响, 不然结果会是一个,随机的结果

# 2.1.7 Understanding Dropout

  1. dropout 使得神经网络不依赖于某个输入单元,不会给他过多的权重,因为它随时有可能被清除
  2. dropout 可以为每一层设置不同的 keep-prob,我们经常为隐藏单元比较多的层设置较小的 keep-prob,以缓和该层的过拟合现象,而过拟合现象不严重的层可以设置为较高的数值,比如 0.9、1.0,1.0 的话就表示该层将全部保留
  3. 不过这也带来一个问题就是,我们要调的超参数更多了
  4. 我们通常在计算机视觉上使用 dropout
  5. dropout 一大缺点是成本函数 J 不再被明确定义,因为每次都是随机的,我们很难明确的定义 J 使之每次迭代都下降,所以我们可以关闭 dropout,也就是 keep-prob 设为 1,确保 J 单调递减再打开 dropout

# 2.1.8 Other regularization methods

  1. 数据扩增 再拿猫的识别举例子,当我们数据不够时,我们可以将猫的图片水平翻转或者转动一定角度、放缩等操作,当然我们要保证他还是一只猫,比如竖直翻转什么的就是有点蠢的操作了
  2. early stopping 就是在过拟合之前就、停止继续训练了,但是这个缺点也很明显,就是很可能停下来的位置的 J 还是很高,但为了防止过拟合不得不过早停下来

# 2.1.9 Normalizing inputs

归一化输入,它包括这么两步:

  1. 零均值 μ=1mi=1mx(i)\mu=\frac{1}{m}\sum_{i=1}^{m}x^{(i)} x=μx-=\mu

  2. 归一化方差 σ2=1mi=1mx(i)2\sigma^2=\frac{1}{m}\sum_{i=1}^mx^{(i)}**2 x/=σ2x/=\sigma^2

    DeepLearning07

  • 这样我们就得到了一个,均值为 0,方差为 1 的输入特征,当然,我们要对每个输入特征都进行归一化,这样才能让每一个输入都很……嗯……规范……那么有什么好处呢???

  • 我们首先看上面第一个“碗”,很明显,这是一个长相很奇怪的代价函数,而且它不是归一化的,如果我们使用梯度下降,那么他将不断地沿着梯度最大(这里体现为“等高线”的法线方向)崎岖的、蜿蜒的路走下去,而且这样我们没办法使用过大的步长(学习率),否则很难到达“终点”

  • 我们再看第二个圆整的“碗”,我们直观上会很喜欢这样的一个碗,因为它很标准,每个方向上都是这样,任何一个初始值都会直接奔向“终点”,这使得 J 的优化会加快很多

  • 另外,我们是在各特征的范围差距比较大时使用这个是非常必要的,如果各输入特征范围相似度比较高,那么我们使用归一化就不是那么重要了

# 2.1.10 Vanishing / Exploding gradients

为什么会产生梯度消失和梯度爆炸现象呢?看下面的例子

  1. 我们有那么一个层数很多的,线性激活的矩阵,我们假设每层的参数 w 都是稍稍小于 1 的对角矩阵,b 当 0 处理,嗯,w 只需要稍稍小于 1,当传递到最后一层会是什么情况呢?不用说也能想象得到,那么我们又如何得到梯度优化参数呢?
  2. 就是这样的过程,相反地,如果参数 w 稍稍大于 1,那么最后一层的数据将会是爆炸式的

# 2.1.11 Weight Initialization for Deep NetworksVanishing / Exploding gradients

  1. 如果是一个神经元,我们可能不会希望随着输入层数量的增加而产生过高的输出,所以我们也许会希望除以输入层特征的数量

  2. 让我们来计算一下,以免看不懂后面的根号是怎么来的

    1. 然后就算自闭了……

    2. z = wx+b = w1x1+w2x2+...+wnxnw_1x_1+w_2x_2+...+w_nx_n

    3. a=σ(z)a = \sigma(z)

    4. D(z)D(z) =D(i=1nwixi)=D(\sum_{i=1}^nw_ix_i) =i=1nD(wixi)=\sum_{i=1}^nD(w_ix_i) # 相互独立 =nD(wixi)=nD(w_ix_i) # 同分布

    5. D(wixi)D(w_ix_i) =E(wi2xi2)E2(wi)E2(xi)=E(w_i^2x_i^2)-E^2(w_i)E^2(x_i) =E(wi2)E(xi2)E2(wi)E2(xi)=E(w_i^2)E(x_i^2)-E^2(w_i)E^2(x_i)

      假设: xx已经均一化 => E(xi)=0,D(xi)=1E(x_i)=0,D(x_i)=1 > ww是以 0 为均值进行的初始化 => E(wi)=0E(w_i)=0

      =D(wi)=D(w_i)

    6. D(z)=nD(wi)D(z)=nD(w_i)

      我们更期望每层得到的方差不会累积增加,维持在“1”这个 Just right 刚刚好(与D(xi)D(x_i)一样)

      D(wi)=1nD(w_i)=\frac{1}{n} # 根据D(aX)=a2D(X)D(aX)=a^2D(X)很容易得出,wiw_i应每个除n\sqrt{n}

    7. 完了???啊不对,我在算什么啊,下一层可不是 z 哦,下一层是 a 啊,那好说,其实上面的过程是使用线性激活的情况,非线性激活也是一样的步骤啦,下面试着推下 ReLU 作为激活函数的情况

    8. D(a)D(a) =D{i=1nReLU(wixi)}=D\{\sum_{i=1}^nReLU(w_ix_i)\} =nD{ReLU(wixi)}=nD\{ReLU(w_ix_i)\}

    9. D{ReLU(wixi)}D\{ReLU(w_ix_i)\} =E{ReLU2(wixi)}E2{ReLU(wixi)}=E\{ReLU^2(w_ix_i)\}-E^2\{ReLU(w_ix_i)\} =12E(wi2xi2)+12E(0)[12E(wixi)+12E(0)]2=\frac{1}{2}E(w_i^2x_i^2)+\frac{1}{2}E(0)-[\frac{1}{2}E(w_ix_i)+\frac{1}{2}E(0)]^2 =12E(wi2)E(xi2)14E2(wixi)=\frac{1}{2}E(w_i^2)E(x_i^2)-\frac{1}{4}E^2(w_ix_i) =12D(wi)=\frac{1}{2}D(w_i)

    10. D(a)=n2D(wi)D(a)=\frac{n}{2}D(w_i) D(wi)=2nD(w_i)=\frac{2}{n}

    11. 就是这样,我们就算出来了 ReLU 的随机初始化方差应该取2n\frac{2}{n},也就是W[l]=np.random.randn(shape)np.sqrt(2n[l1])W^{[l]}=np.random.randn(shape)*np.sqrt(\frac{2}{n^{[l-1]}})

    12. 当然,这是 ReLU 的情况,它是使用2n[l1]\sqrt{\frac{2}{n^{[l-1]}}},还有其他的激活函数呢,对于 tanh,有人说使用1n[l1]\sqrt{\frac{1}{n^{[l-1]}}},也有人提到使用2n[l1]+n[l]\sqrt{\frac{2}{n^{[l-1]}+n^{[l]}}}

    13. 不过,权重的初始化只是一个起点,这些公式只能给一个好的起点,他们会给出初始化权重矩阵的方差的默认值,如果想增加方差,那么方差会成为另一个超参数,这可以通过在刚才式子上增加一个调优参数实现,但这并不是我们的首要调优参数,所以,我们往往将它的优先级放的比较低。

    14. 边复习边算的,很多东西都忘了,可信度不高(~ ̄ ▽  ̄)~

# 2.1.12 Numerical approximation of gradients

使用导数定义近似估计梯度,首先我们看下单边误差

limε0f(θ+ε)f(θ)εf(θ)εk=limε0f(θ+ε)f(θ)εf(θ)εk+1=limε0f(θ+ε)f(θ)(k+1)εk=limε0f(θ+ε)k(k+1)εk1 \begin{aligned} & \lim \limits_{\varepsilon \to 0} \frac{\frac{f(\theta + \varepsilon) - f(\theta)}{\varepsilon} - f'(\theta)}{\varepsilon ^k} \\ =& \lim \limits_{\varepsilon \to 0} \frac{f(\theta + \varepsilon) - f(\theta) - \varepsilon f'(\theta)}{\varepsilon ^{k+1}} \\ =& \lim \limits_{\varepsilon \to 0} \frac{f'(\theta + \varepsilon) - f'(\theta)}{(k+1)\varepsilon ^k} \\ =& \lim \limits_{\varepsilon \to 0} \frac{f''(\theta + \varepsilon)}{k(k+1) \varepsilon ^{k-1}} \end{aligned}

k1=0k-1 = 0 即误差是 ε\varepsilon 的同阶无穷小

下面看看双边误差

limε0f(θ+ε)f(θε)2εf(θ)εk=limε0f(θ+ε)f(θε)2εf(θ)2εk+1=limε0f(θ+ε)+f(θε)2f(θ)2(k+1)εk=limε0f(θ+ε)f(θε)2k(k+1)εk1=limε0f(θ+ε)+f(θε)2(k1)k(k+1)εk2 \begin{aligned} & \lim \limits_{\varepsilon \to 0} \frac{\frac{f(\theta + \varepsilon) - f(\theta - \varepsilon)}{2 \varepsilon} - f'(\theta)}{\varepsilon ^k} \\ =& \lim \limits_{\varepsilon \to 0} \frac{f(\theta + \varepsilon) - f(\theta - \varepsilon) - 2 \varepsilon f'(\theta)}{2 \varepsilon ^{k+1}} \\ =& \lim \limits_{\varepsilon \to 0} \frac{f'(\theta + \varepsilon) + f'(\theta - \varepsilon) - 2 f'(\theta)}{2 (k+1)\varepsilon ^k} \\ =& \lim \limits_{\varepsilon \to 0} \frac{f''(\theta + \varepsilon) - f''(\theta - \varepsilon)}{2 k(k+1) \varepsilon ^{k-1}} \\ =& \lim \limits_{\varepsilon \to 0} \frac{f'''(\theta + \varepsilon) + f'''(\theta - \varepsilon)}{2 (k-1)k(k+1) \varepsilon ^{k-2}} \end{aligned}

k2=0k-2 = 0 即误差是 ε2\varepsilon^2 的同阶无穷小

也就是说,双边误差比单边误差小,所以我们会用双边误差做梯度检验

# 2.1.13 Gradient checking

  1. 就是利用刚刚所说的双边误差对 backprop 的梯度进行检验

    DeepLearning09

  2. 按某种规则用θ\theta将所有参数(w1b1w2b2...wnbnw_1\ b_1\ w_2\ b_2\ ...\ w_n\ b_n)组织起来,嗯,是一个很大的向量,按同样规则用dθd\theta将所有反向传播参数(dw1db1dw2db2...dwndbndw_1\ db_1\ dw_2\ db_2\ ...\ dw_n\ db_n

  3. dθapprox[i]=J(θ1,θ2,...θi+ε...)J(θ1,θ2,...θiε...)2εd\theta_{approx}[i]=\frac{J(\theta_1,\theta_2,...\theta_i+\varepsilon...)-J(\theta_1,\theta_2,...\theta_i-\varepsilon...)}{2\varepsilon},这里的ε\varepsilon只在θi\theta_i上加,也就是只对θi\theta_i求的偏导(Jθi\frac{\partial J}{\partial \theta_i},用双边的导数定义近似求解),相应地,因为dθd\theta也是这样维度的向量,dθ[i]d\theta[i]就表示的是我们自己根据反向传播计算的对θi\theta_i的求导(dw1db1dw2db2...dwndbndw_1\ db_1\ dw_2\ db_2\ ...\ dw_n\ db_n都是一步步按反向传播计算出来的,虽然现在按照某种变换改变了,但他们都是对 J 的导数是没有问题的)

  4. 我们要求dθapproxd\theta_{approx}dθd\theta,但我们现在只有各个偏导组成的向量,要怎么度量两个向量是否彼此接近呢?我们可以使用dθapprox[i]dθ[i]d\theta_{approx}[i]-d\theta[i]的欧几里得范数dθapproxdθ2||d\theta_{approx}-d\theta||_2,然后对向量的长度进行归一化,也就是dθapproxdθ2dθapprox2+dθ2\frac{||d\theta_{approx}-d\theta||_2}{||d\theta_{approx}||2+||d\theta||_2},Python 的话,可以使用 np.linalg.norm(grad) 来求范数

  5. 我们看下这个值是否有问题,可以试着将它与10710^{-7}比较一下,一般比它小的话就说明导数很可能是正确的,但如果高于它,很可能是出 bug 了,去调试吧!

# 2.1.14 Gradient Checking Implementation Notes

  1. 因为梯度检验真的只是对梯度进行检验,没必要在训练的时候也进行,它只是帮我们调试与排除一个 bug 的方法,对训练本身并没有用处,如果真的在训练的时候使用了的话,很可能会花费大量的时间在上面
  2. 梯度检验也可以帮助我们大概地定位 bug 的位置,因为如果某个地方出 bug 了,那附近的dθapproxd\theta_{approx}dθd\theta相差的会比较大
  3. 如果使用正则化,那么在梯度检验的时候不要忘了正则项
  4. 如果使用 dropout,需要先把 dropout 关闭,然后再进行梯度检验
  5. 有的时候会出现这样的情况:wwbb比较小时,梯度下降是正确的,运行梯度下降的时候,wwbb会变得更大,会越来越不准确,为了防止出现这种情况,需要在随机初始化过程中运行梯度检验,训练网络一段时间后再运行梯度检验

# 2.2 Optimization algorithms

# 2.2.1 Mini-batch gradient descent

Mini-batch?好奇怪的名字,但如果我们把以前的方法称作 batch,Mini-batch 就很好理解了。

  1. batch 每次是使用整个样本集 m 条数据对网络进行训练,但当 m 比较大的时候呢?这时每次训练完整个样本才能进行一次的梯度下降法,效率就会很低
  2. 当样本集数量比较大的时候,我们将样本集拆分为一个个的mini-batch,我们每对一个 mini-batch 使用梯度递法,这样会很有效地加快训练的速度
  3. 比如说有 5,000,000 个样本数据,我们可以将x(1)x^{(1)}x(1000)x^{(1000)}作为第一个 mini-batch,我们把它记作X{1}X^{\{1\}},同样地,x(1001)x^{(1001)}x(2000)x^{(2000)}称为X{2}X^{\{2\}}and so on.直到X{5000}X^{\{5000\}},相应的,对 y 也做同样的处理;当然,我们现在需要用一个 for 循环来遍历整个训练集了
  4. 我们现在需要如何计算成本?因为自己规模是 1000,J=11000i=1lL(y^(i),y(i))J=\frac{1}{1000}\sum_{i=1}^lL(\hat{y}^{(i)},y^{(i)}),当然,L(y^(i),y(i))L(\hat{y}^{(i)},y^{(i)})是来自 mini-batchX{t}X^{\{t\}}Y{t}Y^{\{t\}}中的样本
  5. 如果用到了正则化,可以使用J=11000i=1lL(y^(i),y(i))+λ21000lω[l]F2J=\frac{1}{1000}\sum_{i=1}^lL(\hat{y}^{(i)},y^{(i)})+\frac{\lambda}{2\cdot1000}\sum_l||\omega^{[l]}||_F^2

# 2.2.2 Understanding mini-batch gradient descent

batch 梯度下降时每次迭代成本都会下降(除非学习率过大导致在最优解处不收敛),但如果是 mini-batch,我们更可能看到的是一个整体趋势减小但噪声很大的成本

  1. 我们如果选取 1 作为 mini-batch 的话,噪声会非常非常的大
  2. 如果选取 m 作为 mini-batch 也就是 m 的话,也就是 batch 梯度下降法,噪声是小了,但是单次迭代耗时太长
  3. 我们通常在样本小于 2000 的时候直接使用 batch 梯度下降法如果大于 2000 的时候设 mini-batch 为 64 到 512,因为考虑到电脑内存的设置和使用方式,我们最好使用这些 2 的 n 次方型数据
  4. 最后需要注意的一点是X{t}X^{\{t\}}Y{t}Y^{\{t\}}需要符合 CPU/GPU 内存

# 2.2.3 Exponentially weighted averages

指数加权平均,也叫指数加权移动平均

  1. 首先,我们有这么一堆数据,哦,看起来像一些散点图,那么如何求他们的指数加权平均呢
v0=0v1=0.9v0+0.1θ1v1=0.9v1+0.1θ2v1=0.9v2+0.1θ3vt=0.9v(t1)+0.1θt \begin{aligned} v_0 & = 0 \\ v_1 & = 0.9v_0+0.1\theta_1 \\ v_1 & = 0.9v_1+0.1\theta_2 \\ v_1 & = 0.9v_2+0.1\theta_3 \\ & \cdots \\ v_t & = 0.9v_{(t-1)}+0.1\theta_t \\ \end{aligned}
  • 这里θt\theta_t表示第 t 天的数据,vtv_t就是指数加权平均数
  • 如果我们令这里的 0.9 为β\beta,那么我们会得到
v0=0vt=βv(t1)+(1β)θt \begin{aligned} v_0 & = 0 \\ v_t & = \beta v_{(t-1)} + (1-\beta)\theta_t \end{aligned}
  • 可以看做最近11β\frac{1}{1-\beta}天的平均值
  1. β\beta越大每个数值平均的天数越多,曲线越光滑,β\beta越小每个数值平均的天数越少,故曲线噪声很大

# 2.2.4 Understanding exponentially weighted averages

  1. 我们依然使用β=0.1\beta=0.1作为例子

    v100=0.1θ100+0.1×0.9θ99+0.1×0.92θ98+0.1×0.93θ97+0.1×0.94θ96+v_{100} = 0.1\theta_{100} + 0.1\times0.9\theta_{99} + 0.1\times0.9^2\theta_{98} + 0.1\times0.9^3\theta_{97} + 0.1\times0.9^4\theta_{96} + \cdots

  • v100v_{100}展开后很容易看出来这是θt\theta_t与一个指数函数相乘(β(1β)t\beta (1-\beta)^t,这里以 100 天为原点,向负方向增长)
  • 那么为什么是表示最近11β\frac{1}{1-\beta}的平均值呢,我们推一下
    1. 首先,我们知道β\beta是一个 0-1 的数,它更趋近于 1(越趋近于 0 噪声越大,指数加权平均的意义越小)
    2. limn(1+1x)x=e\lim_{n\rightarrow\infty} (1+\frac{1}{x})^x=e,emmm,很基本的极限公式
    3. limβ1β11β=lim(β1)0[1+(β1)]1(β1)=1e\lim_{\beta\rightarrow1^-}\beta^{\frac{1}{1-\beta}}=\lim_{(\beta-1)\rightarrow0^-}[1+(\beta-1)]^{-\frac{1}{(\beta-1)}} = \frac{1}{e}
    4. 也就是说11β\frac{1}{1-\beta}天后的权重降低到了(1β)1e(1-\beta)\cdot\frac{1}{e}之下,可以说是一个足够小的值了
  1. 不过,我们如何计算它呢?
v = 0
for loop{
    v = beta * v + (1-beta) * theta_t
}
1
2
3
4
  1. 由上面的实现方法我们可以知道这种算法很少占用内存,计算的时候只需要读入一个数据并使用计算后的结果覆盖掉原来的数据

# 2.2.5 Bias correction in exponentially weighted averages

这里讲下对早期数据偏差大的一个优化方案

  1. 很明显,令v0=0v_0=0的话,起点会有点偏低,这导致前期的数据整体处于一个偏低的趋势,当然,后期数据会逐渐弱化掉前期数据的影响,但如果要考虑前期数据的话,我们需要做一下调整
  2. 其实也没啥,就是令我们原来求得的vtv_t变成vt1βt\frac{v_t}{1-\beta^t},那么v1=βv0+(1β)θ11β1=θ1v_1=\frac{\beta*v_0+(1-\beta)\theta_1}{1-\beta^1}=\theta_1,嗯,第一个值已经修正到θ1\theta_1了,那么后期数据呢?当t+t\rightarrow+\infty时,修正分母趋近于 1,和原曲线是重合的
  3. 注意,是使原来的数据除以1βt1-\beta^t进行修正,修正后的数据并不会参与之后的运算,伪码描述大概是这样:
v = 0
for loop{
    v = beta * v + (1-beta) * theta_t
}
v /= 1-beta**t // 注意,不是放在forloop内的哦
1
2
3
4
5

# 2.2.6 Gradient descent with Momentum

动量梯度下降法?听着很好玩,让我们来看看到底是个什么东西

  1. 我们使用梯度递降法的时候经常会遇到这种情况:

    DeepLearning10

    每次梯度递降都会不断地波动,指向的方向并不是最终的位置,当然,增大学习率会使得这个现象更加严重

    我们更希望图中竖直方向上的波动更小些,水平方向移动更快些,要如何做呢?

  2. 我们试试这个方法:

vdW=βvdW+(1β)dWvdb=βvdb+(1β)db \begin{aligned} v_{dW} & = \beta v_{dW} + (1-\beta)dW \\ v_{db} & = \beta v_{db} + (1-\beta)db \end{aligned}

当然,我们后来梯度递降时候也相应地变成了:

W=WαvdWb=bαvdb \begin{aligned} W & = W - \alpha v_{dW} \\ b & = b - \alpha v_{db} \end{aligned}
  • 不过这有什么好处呢?很明显,在梯度递降的过程中,由于移动平均值的相互抵消,我们在纵轴方向上的移动变缓了,又由于α\alpha是不变的,我们水平方向上必然会变快,所以最终得到的曲线会像红线这样:

    DeepLearning11

  1. 不过为啥叫动量梯度递降呢?我们可以这样理解:
    • 一个小球从山上向下滚
    • vdW=βvdW+(1β)dWv_{dW} = \beta v_{dW} + (1-\beta)dW为例
    • vdWv_{dW}看作是速度,dWdW看作是加速度,β\beta看作一个阻力的系数吧,也可以理解成摩擦力什么的,不过这不太严谨
    • 很明显,移动平均后的值使得每次梯度递降的部分受到前几次的影响,类似于速度这样的“连续”的物理量,不像单纯的梯度递降法每次都是独立的
  2. 怎么计算呢?我们现在在梯度递降的过程中有了α\alphaβ\beta这两个超参数,嗯,计算方法前面已经写出来了,这里再整理下:
vdW=βvdW+(1β)dWvdb=βvdb+(1β)dbW=WαvdW,b=bαvdb \begin{aligned} v_{dW} & = \beta v_{dW} + (1-\beta)dW \\ v_{db} & = \beta v_{db} + (1-\beta)db \\ W & = W - \alpha v_{dW}, b = b - \alpha v_{db} \end{aligned}
  • 我们经常取β=0.9\beta=0.9
  • 至于偏差修正,我们很少使用它,因为那只影响前几次,如果取β=0.9\beta=0.9的话,那只影响前十次的vdWv_{dW},所以并没有太大必要去使用它
  • 有时我们会看到去掉(1β)(1-\beta)的公式,emmm,就是:
vdW=βvdW+dWvdb=βvdb+dbW=WαvdW,b=bαvdb \begin{aligned} v_{dW} & = \beta v_{dW} + dW \\ v_{db} & = \beta v_{db} + db \\ W & = W - \alpha v_{dW}, b = b - \alpha v_{db} \end{aligned}

当然,它的原理还是 Momentum 法,只不过把(1β)(1-\beta)隐藏到后面的α\alpha里了(后面的推导没太大意义,可以略过),当然其实β\beta也是变了的,我们先只看dWdW,把这个式子改写成:

vdW=βvdW+dWW=WαvdW=Wα(βvdW+dW) \begin{aligned} v_{dW} & = \beta' v_{dW} + dW \\ W & = W - \alpha' v_{dW} = W - \alpha'(\beta'v_{dW} + dW) \end{aligned}

与原来式子相比,我们可以知道:

α(βvdW+dW)=α(βvdW+(1β)dW) \alpha'(\beta'v_{dW} + dW) = \alpha(\beta v_{dW} + (1-\beta)dW)

易知:

α=αβ+αβ=αβα=αβαβ+α \begin{aligned} \alpha & = \alpha'\beta'+\alpha' \\ \beta & = \frac{\alpha'\beta'}{\alpha} = \frac{\alpha'\beta'}{\alpha'\beta'+\alpha'} \end{aligned}

很明显,原来的α\alphaβ\beta是可以用现在的α\alphaβ\beta表示出来的,当然,这和原来的式子可以说没有任何区别,但是这个式子表意并不如原来的明确

  1. 当然,我们在 batch 或者 mini-batch 都可以使用 Momentum 的方法

# 2.2.7 RMSprop

和 Momentum 的图一样,我们参考那张图下

  1. 我们假设横轴代表WW,纵轴代表bb,那么我们使用下面的方法试试:
SdW=βSdW+(1β)dW2Sdb=βSdb+(1β)db2W=WαdWSdW,b=bαdbSdb \begin{aligned} S_{dW} & = \beta S_{dW} + (1-\beta)dW^2 \\ S_{db} & = \beta S_{db} + (1-\beta)db^2 \\ W & = W - \alpha \frac{dW}{\sqrt{S_{dW}}}, b = b - \alpha \frac{db}{\sqrt{S_{db}}} \end{aligned}
  1. 我们直观上这么理解这个方法:
    • 如果纵轴bb上移动过快,那么SdbS_{db}会是一个比较大的值,bb梯度下降的梯度下降就会降低,WW也是一样的,这样做就使得某一项移动不会过快,以消除摆动
    • 当然,这里只是为了简单只考虑了WWbb,事实上WW也是一个高维度的量,要考虑W1,W2,W3,W_1, W_2, W_3, \cdots
    • 为了防止分母为 0,我们经常使用在分母上加一个比较小的值ε\varepsilon(通常取10810^{-8}

# 2.2.8 Adam optimization algorithm

Adam 优化算法,简单的说,这是一个结合了 Momentum 算法和 RMSprop 算法的算法,直接上算法,相信大家也明白

  • Initialize:Initialize :

  • vdW=0,SdW=0,vdb=0,Sdb=0v_{dW} = 0, S_{dW} = 0, v_{db} = 0, S_{db} = 0

  • vdW=β1vdW+(1β1)dW,vdb=β1vdb+(1β1)dbv_{dW} = \beta_1v_{dW} + (1-\beta_1)dW, v_{db} = \beta_1v_{db} + (1-\beta_1)db

  • SdW=β2vdW+(1β2)dW2,Sdb=β2vdb+(1β2)db2S_{dW} = \beta_2v_{dW} + (1-\beta_2)dW^2, S_{db} = \beta_2v_{db} + (1-\beta_2)db^2

  • vdWcorrected=vdW(1β1t),vdbcorrected=vdb(1β1t)v_{dW}^{corrected} = \frac{v_{dW}}{(1-\beta_1^t)}, v_{db}^{corrected} = \frac{v_{db}}{(1-\beta_1^t)}

  • SdWcorrected=SdW(1β2t),Sdbcorrected=Sdb(1β2t)S_{dW}^{corrected} = \frac{S_{dW}}{(1-\beta_2^t)}, S_{db}^{corrected} = \frac{S_{db}}{(1-\beta_2^t)}

  • W=WαvdWcorrectedSdWcorrected+ε,b=bαvdbcorrectedSdbcorrected+εW = W - \alpha\frac{v_{dW}^{corrected}}{\sqrt{S_{dW}^{corrected}}+\varepsilon}, b = b - \alpha\frac{v_{db}^{corrected}}{\sqrt{S_{db}^{corrected}}+\varepsilon}

  • Adam 通常是使用偏差修正的

  • Momentum 和 RMSprop 的参数β\beta是不一样的,故使用β1\beta_1β2\beta_2以区分

  • 这是一种能够适用于不同神经网络的算法

  • 这里这么多超参数,我们如何设置超参数呢?

    1. α\alpha,这是要经常调整的
    2. β1\beta_1,Adam 论文作者推荐 0.9
    3. β2\beta_2,Adam 论文作者推荐 0.999
    4. ε\varepsilon,Adam 论文作者推荐10810^{-8}

# 2.2.9 Learning rate decay

很明显,我们学习率过大的时候很难收敛在最优解处,这时候如果我们使用递减的学习率,在接近最优解附近学习率已经很小了,就很容易收敛

下面是几种常用的计算方法

  1. α=11+decayrateepochnumα0\alpha = \frac{1}{1+decayrate*epoch-num}\cdot\alpha_0
  2. α=0.95epochnumα0\alpha = 0.95^{epoch-num}\cdot\alpha_0
  3. α=kepochnumα0\alpha = \frac{k}{\sqrt{epoch-num}}\cdot\alpha_0
  4. 按阶梯状下降的学习率……
  5. 手动衰减😂

# 2.2.10 The problem of local optima

  1. 在我们的想象中,成本函数 J 和参数 W 之间的关系也许会更像这种形式:

    DeepLearning12

    也就是,有着很多个局部最优的解,这些局部最优点是满足什么条件的呢?很明显,在各个方向的偏导都是 0,而且,在该点的凹凸形式也是一样的才行,不过在三维空间上这种情况还算是比较常见的,如果是在高维情况下,这种要求可以说是很苛刻的,如果在某一个点满足了各个方向偏微分是 0,那么我们更多遇到的是这种情况:

    DeepLearning13

    也就是一个鞍点,很难碰到一个局部最优,因为在高维情况下这太难得了(2n2^n

  2. 虽然不会长时间困在局部最优的位置,但是,我们很可能长时间处在平缓段,就像下面这样:

    DeepLearning14

因为梯度真的很小,所以梯度递降法会使它小心翼翼地沿着“脊”处缓缓移动,直到鞍点处出现微小扰动后才向正确方向走去,这种情况下,Adam 算法可以让它更快地度过平缓段

  1. 对高维的实在是难以直观地去理解,不过我们对它们的理解正在不断发展……………………

# 2.3 Hyperparameter tuning

# 2.3.1 Tuning process

首先,我们先把我们要调整的各个超参数都列出来:

  • α\alpha
  • β\beta
  • β1\beta1, β2\beta2, ε\varepsilon
  • #layerslayers
  • #hiddenunitshidden\ units
  • learningratedecaylearning\ rate\ decay
  • minibatchsizemini-batch\ size

我们最优先考虑的一定是α\alpha,其次呢,就是 Momentum 参数β\beta了,还有minibatchsizemini-batch\ size和#hiddenunitshidden\ units,最后考虑#layerslayerslearningratedecaylearning\ rate\ decay

但是我们到底怎么测试超参数怎么取值是比较好的呢?我们可以对考虑的几个超参数进行随机取值,然后在结果比较理想的区域(几个超参数就是几个维度的空间)重新随机取值,就像拿着“放大镜”找东西那样不断地越来越精细地搜索最佳超参数

# 2.3.2 Using an appropriate scale to pick hyperparameters

虽然说随机取值是个不错的方法,但是这也是要看情况的,我们要根据具体情况选择合适的标尺

  • 如果说我们要在 50 到 100 内选#hiddenunitshidden\ units,或者在 2 到 4 内选#layerslayers,那么很明显,这种随机取值的很合适的
  • 我们再看这个:如果我们要在 0.0001 到 1 内选一个α\alpha,如果我们真的像这样随机选取,那么很明显,有 90%的数据是落在 0.1~1 之间,而 0.0001 到 0.1 只有 10%,这明显不是我们要的,看到这个我们很容易想到 lg()函数,事实上我们确实也是这么做的,这里只需要这么做:
    r = -4 * np.random.rand()
    alpha = 10 ** r
    
    1
    2
  • 相反地,我们要怎么取β\beta呢?这个数很明显是一个接近于 1 的值,嗯,很容易想到了,其实就是1α1-\alpha的方式

# 2.3.3 Pandas VS Caviar(Hyperparameters tuning in practice: Pandas vs. Caviar)

Pandas:没有足够的计算资源,同时只能计算一组超参数

Caviar:计算资源足够,同时计算多组超参数

# 2.3.4 Normalizing activations in a network

我们之前已经了解到这么一种算法叫做……归一化输入是吧,事实上这确实是一个非常有用的算法,它甚至是可以用在深层网络之中,这个时候我们叫它——Batch 归一化(简称 BN

  1. 我们将归一化zz作为默认选择,而不是归一化aa,不过,这也是有争论的
  2. 每次归一化只针对某一层,绝对不可能会将某两层数据归一化处理
  3. 具体操作呢?
μ=1miz(i)σ2=1mi(ziμ)2znorm(i)=z(i)μσ2+ε \begin{aligned} \mu & = \frac{1}{m} \sum_i z^{(i)} \\ \sigma^2 & = \frac{1}{m} \sum_i (z_i - \mu)^2 \\ z_{norm}^{(i)} & = \frac{z^{(i)}-\mu}{\sqrt{\sigma^2 + \varepsilon}} \end{aligned}
  1. 这样,这些zz就均一化为均值 0 方差 1 咯~
  2. 但是,我们也许并不希望zz的每个分量都是均值 0 方差 1 这样千篇一律的状态,也许隐藏单元需要更加多元化的分布,所以,我们可以使用
z~(i)=γznorm(i)+β \tilde{z}^{(i)} = \gamma z_{norm}^{(i)} + \beta

这里的γ\gammaβ\beta是模型的学习参数,需要像更新wwbb一样更新它们

  1. 你可以随意地设置z~(i)\tilde{z}^{(i)}的均值,如果γ=σ2+ε\gamma = \sqrt{\sigma^2 + \varepsilon}而且β=μ\beta = \mu,那么z~(i)=z(i)\tilde{z}^{(i)} = z^{(i)},也就是说,z(i)z^{(i)}只是某种γ\gammaβ\beta取值下的z~(i)\tilde{z}^{(i)}
  2. 隐藏单元和训练输入归一化的一个区别是,隐藏单元也许并不一定是均值 0 方差 1
  3. γ\gammaβ\beta的作用是使隐藏单元值的均值和方差标准化,即z(i)z^{(i)}有固定的均值和方差,至于是多少……当然是要训练的呀~

# 2.3.5 Fitting Batch Norm into a neural network

  1. 之前说过,每一个神经网络单元所做的无非计算z[l]z^{[l]}a[l]a^{[l]}两步,而如果使用 Batch 归一化的话,我们要在这两步之间加一步计算z~[l]\tilde{z}^{[l]},之后再据此计算a[l]a^{[l]}
  2. 所以说,我们现在每层的参数由w[l]w^{[l]}b[l]b^{[l]}变成了w[l]w^{[l]}b[l]b^{[l]}β[l]\beta^{[l]}γ[l]\gamma^{[l]}四个,当然这里的β\beta不是 Momentum 里的超参数β\beta
  3. 当然你还是可以结合 Adam 或 RMSprop 或 Momentum 方法而不是单纯地使用 BN
  4. BN 的过程虽然看起来很复杂,但是我们如果在框架里使用它的话,它只是一行代码而已,就比如说 Tensorflow ,我们只需要使用tf.nn.batch_normalization就可以实现它
  5. 实践中经常会将 mini-batch 和 BN 一起使用,
  6. 之前有提到使用 BN 后会有四个参数w[l]w^{[l]}b[l]b^{[l]}β[l]\beta^{[l]}γ[l]\gamma^{[l]},但是事实上b[l]b^{[l]}已经是没有必要的,因为我们的z[l]=w[l]a[l1]+b[l]z^{[l]} = w^{[l]} a{[l-1]} + b^{[l]},但是之后的归一化会将所有的z[l]z^{[l]}变换为均值为 0,方差为 1,再由γ\gammaβ\beta重新缩放,所以b[l]b^{[l]}其实是在这个过程中被消除掉了
  7. 所以,接下来,我们要做的就是反向 prop 也就是
w[l]=w[l]αdw[l]β[l]=β[l]αdβ[l]γ[l]=γ[l]αdγ[l] \begin{aligned} w^{[l]} & = w^{[l]} - \alpha dw^{[l]} \\ \beta^{[l]} & = \beta^{[l]} - \alpha d\beta^{[l]} \\ \gamma^{[l]} & = \gamma^{[l]} - \alpha d\gamma^{[l]} \\ \end{aligned}
  1. 事实上,我们更多的是直接用框架完成这些~

# 2.3.6 Why does Batch Norm work?

为什么 BN 会有用呐?

  1. 第一个原因是,BN 会将各层输入的特征值进行归一化,就和之前的归一化输入是一样的,每层都会得到“标准”的输入,这对加速学习是非常有用的
  2. 另外,它可以使得网络更加滞后或者说更深层,神经网络的后层比之于前层更加经受得住变化
  3. 那么如何经受变化呢?比如说我们一个神经网络适应了某一个分布下的训练集,可以对该分布下训练集做出很好的预测,但是换了一个分布的训练集情况就很有可能会有所不同,这有点像这个神经网络过拟合第一个分布下的训练集,我们称这种情况为 Covariate shift
  4. 我们随便找到一个深层神经网络,比如说一个 5 层的神经网络,我们遮去前两层暂且不看,那么第三层的输入又是什么样子的呢?当然是没有规律的、分布未知的数据,因为我们根本没有对它的输入进行任何的约束,它只是在前层参数下的一个输出,这样下去,每层的噪声将会非常的大,前层的一个调整将会使后层的分布发生改变,这当然不是我们想看到的
  5. 但如果每层的分布是我们可以进行控制的(γ\gammaβ\beta),这样至少可以保证每层的输入在一个可控的分布下,这使得整个神经网络更加稳定,前层的改变迫使后层的适应程度会减小,也就减弱了层与层之间的联系,使得每层都可以自己学习,稍稍独立于其它层
  6. 另外, BN 还有一个作用就是轻微的正则化,为什么呢?主要是因为每组 mini-batch 得到的均值和方差都与整个数据集的有一定的噪声,这就使得得到的z~[l]\tilde{z}^{[l]}也是有噪声的,这就使得它有点类似于 dropout 一样,使得后层并不会过分得依赖于前层,当然这只是轻微的正则化,如果想要获得更好的正则化是可以配合 dropout 使用的
  7. 当然,正则化并不是 BN 的真正用途,这只是一个意外的结果罢了,也许 ��� 给你的训练带来好处,当然也有可能会带来一些问题

# 2.3.7 Batch Norm at test time

  1. 首先回顾下我们计算 BN 的公式
μ=1miz(i)σ2=1mi(ziμ)2znorm(i)=z(i)μσ2+εz~(i)=γznorm(i)+β \begin{aligned} \mu & = \frac{1}{m} \sum_i z^{(i)} \\ \sigma^2 & = \frac{1}{m} \sum_i (z_i - \mu)^2 \\ z_{norm}^{(i)} & = \frac{z^{(i)}-\mu}{\sqrt{\sigma^2 + \varepsilon}} \\ \tilde{z}^{(i)} & = \gamma z_{norm}^{(i)} + \beta \end{aligned}
  1. 既然要计算每个 mini-batch ,那么mm就一定是该 mini-batch 的样本数量而不是整个训练集的样本数量,但是我们可以看到,每处理一个数据都需要先计算出μ\mu,再计算出σ\sigma,然后才能对这一个样本进行处理,哦,天呐,算一个样本的z~(i)\tilde{z}^{(i)}要将整个 mini-batch 遍历三次!
  2. 为了避免这种遍历,要如何做呢?没错,就是指数加权平均,用它来估算μ\muσ\sigma,事实上任何合理的估算方法都是可以的,而在框架里大多都已经有着默认的估算方式,一般都有着比较好的效果

# 2.3.8 Softmax regression

我们之前所用的都是二分类,比如说输入一张图片分类为是或不是猫,但是很多情况下我们需要做到更多,比如说,输入一张图片分类为猫、狗、鸡或者其他,这要如何做呢?

  1. 当然就是 Softmax 啦,之前我们之所以输出的是二分类当然是因为我们在输出层应用的是 Logistic 回归,而且输出层也只设置了一个结点,那么我们只需要在这里进行一些改动,就可以将它变成一个更强大的分类器啦
  2. 比如接着刚才那个例子,将图片分为猫、狗、鸡、或者其他,就是说需要 C = 4 种分类,那么我们可以设置这样的四个结点,每个结点输入z[l]z^{[l]},经过非线性变换后变成ez[l]e^{z^{[l]}},临时叫它t[l]t^{[l]},因为我们最终要获得每种的概率,所以我们还要将这四个结点的输出进行归一化,也就是都除以他们的和,a[l]=t[l]j=03t[l]a^{[l]} = \frac{t^{[l]}}{\sum_{j=0}^3 t^{[l]}}
  3. 上面的过程就是 Softmax 回归啦

# 2.3.9 Training a Softmax classifier

事实上在 C = 2 时, Softmax 回归就变成了 logistic 回归,但在 Softmax 中都要怎么做呢?

  1. 损失函数,通常使用L(y^,y)=j=14yjlogy^jL(\hat{y}, y) = -\sum_{j=1}^4 y_j log\hat{y}_j
  2. 反向传播,只需要记住dz[l]=y^ydz^{[l]} = \hat{y} - y,剩下的慢慢求偏导就好啦
  3. 当然,我们马上就要学习框架了,这使得只需要我们更加注重前向传播的逻辑与设计,而反向传播的求导什么的交给框架来做就好了

# 2.3.10 Deep Learning frameworks

选择框架的标准:

  1. 简单易用,能够将线性代数库做较好的抽象
  2. 运行速度,这是我们一直都有提到的问题
  3. 开源

一些推荐:

  • Caffe / Caffe2
  • CNTK
  • DL4J
  • Keras
  • Lasagne
  • mxnet
  • PaddlePaddle
  • TensorFlow
  • Theano
  • Torch

# 2.3.11 TensorFlow

  1. 首先是一个简单的例子,我们让 TensorFlow 找到使 Cost (w5)2(w-5)^2最小的的参数ww
import numpy as np
import tensorflow as tf
# 定义参数 w
w = tf.Variable(0, dtype=tf.float32)
# 定义 Cost function
cost = tf.add(tf.add(w**2, tf.multiply(-10, w)), 25)
# 定义所用训练方法和学习率,这里是使用梯度递降法与0.01的学习率
train = tf.train.GradientDescentOptimizer(0.01).minimize(cost)
# 下面几行是惯用的写法
# 首先是创建全局变量
init = tf.global_variables_initializer()
# 之后创建一个TensorFlow session
session = tf.Session()
# 初始化全局变量
session.run(init)
# 下面将会将 w 初始化为0,但是并没有运行,所以 print 出来还是0
print(session.run(w))
# 我们运行一步训练过程(这里是梯度递降法)
session.run(train)
print(session.run(w))
# 我们运行1000次再看看
for i in range(1000):
    session.run(train)
print(session.run(w))
# 应该已经很接近最终答案5了
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
  1. 但我们如何像我们之前做的那样将数据喂给它呢?
import numpy as np
import tensorflow as tf
coefficients = np.array([[1.], [-20.], [25.]])
w = tf.Variable(0, dtype=tf.float32)
# 告诉 tf 数据格式,具体数据之后“喂”
x = tf.placeholder(tf.float32, [3, 1])
# 因为 tf 已经重载了很多的运算符,所以我们完全可以将这个式子写得好看些
# cost = w**2 - 10*w + 25
# 为了方便“喂”,把原来固定参数改成 x
cost = x[0][0]*w**2 + x[1][0]*w +x[2][0]
# 如果不想用梯度递降法而是使用 Adam 优化器等等,改掉这行就可以啦
train = tf.train.GradientDescentOptimizer(0.01).minimize(cost)
init = tf.global_variables_initializer()
session = tf.Session()
session.run(init)
print(session.run(w))
# 在这里慢慢“喂”数据,如果要用 mini-batch ,就用 feed_dict 喂入不同的子集
session.run(train, feed_dict={x:coefficients})
print(session.run(w))
for i in range(1000):
    session.run(train, feed_dict={x:coefficients})
print(session.run(w))
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
  1. 也许我们经常遇到 with 的形式,这都是一样的,只不过这样的代码更安全
with tf.Session() as session:
    session.run(init)
    print(session.run(w))
    # And more...
1
2
3
4
  1. 就像前面所说的,所有的反向传播求导法则已经在建立前向传播那行(cost=blabla)建立好了,乘啦加啦什么的,都在框架内做好了对应的求导法则,所以我们要做的就是搭搭前向传播,选选方案,改改超参数啦

  2. 更多请参见TensorFlow 官网

# 3 Structuring Machine Learning Projects

# 3.1 ML strategy(1)

# 3.1.1 Why ML Strategy?

优化的“方向性”问题,如果方向走错了,那么可能是白白浪费了很多的时间

# 3.1.2 Orthogonalization

为什么说正交呢?正交可以理解为垂直的两根轴,在其中一根轴上前进对另一根轴没有分量,这也就是为什么我们学习中都选择直角坐标系而不是选择其它角度的

  1. 我们知道平面内任何不平行的两个矢量都可以将整个平面表示出来,但是,如果是不垂直的两根轴,情况会怎样?
  2. 我们想要的是,调好一根轴上前进的长度,然后安心地调另一根轴,不会改变之前调好的数值,但是我们很不幸的发现,如果两个轴不垂直的话,这种情况是很难的,我们好不容易调好一根轴的数值后,再调第二根轴就会把前面调好的数值打乱,也就是我们很难直观地一次性调好想要的数值,如果想要调好,就一定要左调调、右调调,不断测试
  3. 这才是两根轴,我们经常会遇到更多轴的情况,那可就更复杂了(不敢想象.jpg)
  4. 所以,我们要选取互不影响的几根轴,而且知道他们分别在调什么

哦,说了这么多,我们看看我们的这几根轴吧

  • 无法很好地拟合 training set
    • 更大的 NN
    • 更好的优化算法,比如 Adam 优化算法
  • 无法很好地拟合 dev set
    • 各种正则化
    • 增大 training set
  • 无法很好地拟合 test set
    • (对 dev set 过拟合)回退一步,使用更大的 dev set
  • 无法很好地在生产环境实践
    • 改变 dev set
    • 改变 cost function

      我们很少使用 early stopping ,因为这并不是正交化的一项,它可能会同时影响前两项

# 3.1.3 Single number evaluation metric

我们如何评估一个算法的优劣呢?也许我们有很多的指标,但是如果我们只用单实数作为指标的话,我们的进展就会快很多

  1. 比如说我们有两个分类器 A 、 B ,我们用两个指标来评估他们,一个是 Precision (查准率:在你的分类器标记为猫的例子中,有多少真的是猫)、另一个是 Recall (查全率:对于所有真猫的图片,你的分类器正确识别出了多少百分比)
  2. 都说了两个指标很难评估,这种情况下我们很难取舍啊,A B 分类器各有优劣我们该怎么选?
  3. 有一个标准的方法是使用F1F_1分数进行评估,P 和 R 的调和平均数:21P+1R\frac{2}{\frac{1}{P} + \frac{1}{R}}
  4. 再看另一个例子,比如说我们有一些分类器,在各个地区的评估各有优劣,这种情况下,我们可以尝试使用平均值,这样我们可以快速选出比较好的那个分类器,而不是花很多时间纠结在这上面

# 3.1.4 Satisficing and optimizing metrics

  • 我们看这样的一个例子:
Classifier Accuracy Running time
A 90% 80ms
B 92% 95ms
C 95% 1,500ms

这样我们要怎么评估呢?比如说像上面那样弄个单指数,cost=accuracy0.5×runningTimecost=accuracy-0.5 \times runningTime ,emmmmm,不过这样貌似有点过于刻意了

再想一种方法,比如说,我们要求在运行时间不超过 100ms100ms 的前提下寻找准确率最高的分类器,也就是说,对于运行时间只设定一个阈值,达到这个阈值才满足条件,但达到之后,无论再高也不在乎了,所以这里 BB 的表现最好

更一般地,我们可以考虑下,如果有 NN 个指标的话,我们也许最后只需要留一个指标来做最后的评判,而其余 N1N-1 个指标则只作为阈值就好啦

  • 再看另一个例子

比如说,一个类似于 SiriSiri 的语音助手,我们会将语音识别的准确率作为一个最终评判的指标,但是也会出现些其他的情况,比如说,在没有说激活语句(比如 SiriSiriHeySiriHey\ Siri )的时候它激活了,我们可以称之为假阳性(false positive),所以我们还会有这样一个指标,24 小时内出现假阳性的次数,这个指标的话,我们完全可以将它设定为 24 小时内最多出现一次假阳性

# 3.1.5 Train/dev/test distributions

既然要设训练集、开发集、测试集,那么又要怎么分呢?首先,最重要的一点,一定要记住就是同分布

比如说,我们要做一个猫分类器,拥有着 8 个国家的数据,一个很愚蠢的做法就是拿其中四个国家的数据去做开发集、拿其余四个国家的做测试集,很明显,这是不满足同分布的,通过开发集拟合的数据当然并不能很好地拟合测试集,至于为什么不满足同分布的会导致我们有这样问题呢?看一个小例子:

我们在开发集上不断调优神经网络,使得结果不断地优化,收敛在开发集分布下的最优解下,就相当于我们以这个最优解为靶心不断地射箭,最终能够相对比较准确地射中靶心,但这时候突然把靶心移动了(测试集分布下的最优解),用它来评估准确率,这很明显是不科学的

所以,这种时候我们更多做的是,对这些不同分布的数据进行打乱,随机调出一部分数据作为开发集,随机挑出一部分作为测试集,这样这些数据就满足同分布了

我们在做一个项目的时候一定要将各个分布数据都考虑到,除非那个分布是实际应用中基本不会存在的

# 3.1.6 Size of dev and test sets

嗯,我们原来可能都是这样划分 TrainingSetTraining SetDevSetDev SetTestSetTest Set 的,比如 TrainingSet70%DevSet30%Training Set 70\%\ Dev Set 30\% ,再比如 TrainingSet60%DevSet20%TestSet20%Training Set 60\%\ Dev Set 20\%\ Test Set 20\% 这样,当然这在如今的大数据时代已经不适用了,因为原来我们只有 100~10000 这样的量级的数据,但如今我们有着上百万量级的数据,所以我们可以用更多的数据去训练,比如说 TrainingSet98%DevSet1%TestSet1%Training Set 98\%\ Dev Set 1\%\ Test Set 1\%

由于测试集的作用仅仅是评估已经做好的某个神经网络,更多的数据也仅仅可以提高最终评估结果的置信度,并不能提高其准确度,所以测试集并不是需要太多的数据

有些时候会省略掉测试集,只有训练集和开发集,这样的话如果有足够自信或者真的不需要测试的情况下当然也是可以的,但最好还是设这样的一个测试集,这样可以有效地避免神经网络对开发集过拟合的现象

# 3.1.7 When to change dev/test sets and metrics

  • 举个例子,我们有两个猫分类器,第一个只有 3% 的误差,但是却有很多色情图被当做猫图推送给用户了,而第二个猫分类器则有 5% 的分类器,任何色情图都没有推送给用户,我们还有用户很明显会觉得第二个分类器比较好,但是如果按原来的指标来看,分类器一更佳,这……这说明我们需要改变指标

    如何改变指标?我们可以将 error 的评估表达式中的色情图前面加上一个比较大的权重,这样的话,即便是很少的色情图都会造成很大的误差值,也就避免了上述情况,我们仍能使用单一误差值来评估分类器

  • 另一个例子,比如说我们使用的开发集是在网上收集的质量很好的图片,而实际用户所上传的却是模糊的、摄影角度不专业的图片,这使得原来在开发集上表现优良的分类器一(error=3%)在实际应用中不如分类器二(error=5%),这时候我们需要改变开发集

机器学习完全独立的两步:

  1. 定义评估指标
  2. 优化该指标

# 3.1.8 Why human-level performance?

DeepLearning15

大多数情况下,机器学习随着算法的优化迅速赶超人类的表现,但在超过人类表现之后,进展会越来越缓慢,但是总体上趋近于一个理论上限值——贝叶斯误差,这个上限值是无法超越的,因为总有无法识别的信息(比如说失真十分严重的信息)

为什么是在超越人类表现后进展越来越缓慢了呢?其一是因为人类表现本身已经很接近贝叶斯误差了,再者,数据的来源都是人类,所以我们有着大量人类所创造的数据来用于它准确度的提升,但想要超过人类的话,就无能为力了

# 3.1.9 Avoidable bias

比如有一个分类器,在训练集上误差为 8% ,而在开发集上误差为 10% ,若按以前的想法我们可能会觉得,它的偏差太大了,我们应当使用降低偏差的方法(多跑几次或者使用更大的神经网络)

但如今我们知道了有贝叶斯误差,所以我们现在并不能单纯地认为 8% 是一个很大的误差了,一旦贝叶斯误差是 10% 呢?由于人类的表现已经很接近贝叶斯误差了,所以我们也可以以人类的表现来近似地替代贝叶斯误差

比如人类的表现误差为 1% ,那很明显训练集误差太大了,但如果人类的表现只有 7.5% 呢(比如说这些图片非常非常模糊,人类也很难分辨)?这时候其实训练集已经很接近贝叶斯误差了,此时我们很难继续提高训练集的准确率了,继续提高可能使其过拟合,所以我们这时候应当考虑开发集与训练集为什么会差这 2% 了,也就是考虑方差的问题了

不过我们是怎么判断到底应该考虑偏差还是方差呢?由于原来在训练集上的误差不能单纯地使用了,我们使用一个新的参数——可避免偏差(训练集误差与贝叶斯误差之差),而方差可用开发集与训练集误差之差衡量,在第一个例子里可避免偏差为 7% ,方差为 2% ,很明显我们这时候我们应当使用降低偏差的方法,在第二个例子里可避免偏差为 0.5% ,方差为 2% ,这个时候应当使用降低方差的方法

# 3.1.10 Understanding human-level performance

我们如何评判人类的水平?比如说有一张放射科图片,没经过任何训练的普通人的误差可能达到 3% ,而普通医生可能达到 1% ,经验丰富的医生可能只有 0.7% ,如果这样一队经验丰富的医生相互讨论得到的结果甚至可以达到 0.5% ,但哪个是人类的水平呢?

如果是想要获得贝叶斯误差的话,很明显 0.5% 是最佳选择,因为贝叶斯误差一定比人类最佳表现更好,但如果我们只是想发表论文或者部署系统,那么一个普通医生的误差便可以了(1%)

如果我们面临训练集 5% 的误差、开发集 6% 的误差的话,我们选择哪个作为人类表现都是无所谓的,可避免偏差都比方差大;训练集 1% 的误差、开发集 5% 的误差的话,可避免偏差是一定比方差小的;但如果训练集 0.7% 的误差、开发集 0.8% 的误差,那么贝叶斯误差的近似选择就成了一个棘手的问题了,如果选择的不当,那么方向(降低方差还是降低偏差)就会错误,不过在这个时候训练集和开发集的拟合已经相当不错了,已经相当接近于人类的表现了

# 3.1.11 Surpassing human-level performance

接着之前的例子,单人误差 1% ,一队误差 0.5% ,训练集误差 0.6% ,开发集误差 0.8% ,那么肯定是使用 0.5% 误差的啦,但如果单人误差 1% ,一队误差 0.5% ,训练集误差 0.3% ,开发集误差 0.4% ,可避免偏差是多少?贝叶斯误差选哪个值?如果贝叶斯误差是 0.5% ,那么训练集可能有 0.2% 的过拟合,但贝叶斯误差也是可能比 0.5% 低的啊,那到底是多少呢?0.4%?0.3%?0.2%?这就无从得知了……所以说这样很难评估一个与人类水平不相上下的系统

但是很多系统已经远远超过人类了,比如说广告的推送、物流时间等等,不过这些都只是通过结构化数据训练出的,因为他们有着大量数据的支持,很容易超过人类,而在类似语音识别、图片识别等自然感知任务方面,超过人类还是很难的

# 3.1.12 Improving your model performance

下面就是总结咯

我们的步骤是,第一步要设计合适的网络使得它在训练集上的偏差足够小,也就是尽量减小可避免偏差;第二步就是利用开发集和测试集优化,使得对开发集与测试集的误差尽量得小,也就是尽量减小方差

  1. 减小可避免偏差,我们经常用的策略有

    • 训练更大规模的 NN / 训练更久
    • 选择更好的优化算法(RMSprop、momentum、Adam)
    • 选择其它的神经网络框架(CNN、RNN) / 优化超参数(层数、隐藏单元数等等等等)
  2. 减小方差,我们经常用的策略有

    • 收集更多的数据
    • 正则化(L2L2 正则化、dropoutdropout 正则化、数据增强)
    • 选择其它的神经网络框架 / 优化超参数 (同上)

# 3.2 ML Strategy (2)

# 3.2.1 Carrying out error analysis

如何判断一个算法到底出错在哪里了呢?emmmmm,那我们可以看看它在哪里犯下错误了

比如说,我们这么一个猫分类器,然后它取得了 90% 的准确率,然后我们就要分析那 10% 的错误到底是怎么错的

比如这 10% 有相当一部分是由于把一些狗给分类成猫了,那么我们可以收集些狗的图片用于训练,但如果说狗只占一小部分的话,那么这样就显得很没必要了,所以我们应当考虑到分类错误的各种情况,用一个表格列出来,统计下影响比较大的那几项,并针对其进行处理就可以啦,比如这里我们可以考虑下是否是猫科动物、是否是狗、是否是太模糊、甚至可能是某种滤镜出了问题

# 3.2.2 Cleaning up Incorrectly labeled data

数据中无法避免有一些是被标记错误的,比如说一只猫在一个非常角落的地方不小心漏掉了,再或者把一只画中的猫标记成了真猫,那我们如何处理这些标记错误的数据呢?

如果说标记错误地比较有随机性的话,那么算法的鲁棒性会弥补这个错误,但如果说出现了把所有白色的狗都标记成猫这样的严重错误的话,很明显,最终的算法也会把白色的狗给标记成猫,这个时候我们不得不花些时间去修正它们了

我们可以像上一节一样,将出错部分的数据的出错原因一一列出,也把标记错误这个原因列出来,一样地,我们看下这个原因所占比例如何,如果比例不高,我们可以优先优化其他问题

当然,刚刚对错误的评估仅仅是针对开发集,但如果我们真的要去修正的话,最好开发集和训练集都修正,以保证他们满足同分布,不过仅仅修正开发集也是可以的,因为其分布的差别应当不会特别大

另外,我们修正的时候不要仅仅修正这些发生错误的部分,也要对正确那部分进行修正,因为那些正确的部分也许是侥幸分类正确的啊……

# 3.2.3 Build your first system quickly, then iterate

如果要构建一个全新的机器学习应用的话,最好的方法并不是考虑的太多(这会使得系统一建立便过于复杂),最好是快速划分训练集、开发集,然后迅速建立一个简单的神经网络,并用上周所说的偏差/方差评估法对其进行评估,在评估过程中我们会找出算法的最大缺陷的,然后我们针对该缺陷进行改进

# 3.2.4 Training and testing on different distributions

训练神经网络是要很多很多的数据的,但我们很难收集到和实际应用分布相同的数据,所以我们会尽可能地收集相似的数据,尽管它们的分布与实际应用时差别很大

比如我们要做一个猫分类器,我们从网络上爬取到取景专业、分辨率高的图片(收集到了 20w 组),而我们实际应用的数据(从 APP 上收集的数据)很模糊、取景不专业(也收集到了 1w 组),那要如何划分数据集呢?下面有两种方案,让我们看下

  • 将两种数据随机打乱后,TrainSet21wTrain\ Set\ 21wDevSet5kDev\ Set\ 5kTestSet5kTest\ Set\ 5k ,嗯,都来自同分布了,但是缺点很致命,因为它们与我们的最终目标相差甚远,因为 APP 数据只占很小一部分,所以说我们一直瞄着的方向和我们最终应用的方向会差很多很多,还是看下一种方法吧

  • 训练集中包含全部 20w 爬取数据,此外包含 1w APP 上的数据,而开发集、测试集各 5k 的 APP 上的数据,这就保证了我们后来的目标与最终目标是一致的,只不过训练集和开发集间的分布会相差较大些,但这种方法确实会带来更好的系统性能,至于训练集与开发集分布的差异,后续会讲到优化方案

再举个例子,比如我们要做一个车载语音系统,但真正车载语音数据仅有 2w ,而其他语音系统的数据可以买到 50w 这么多,所以我们可以训练集中包含这 50w 其他数据,而开发集测试集各 1w 车载语音数据,或者使用 50w 其他数据与 1w 车载数据作为训练集, 开发集测试集各 5k 这样

# 3.2.5 Bias and Variance with mismatched data distributions

我们之前都是针对分布相同的数据集评估偏差与方差,那么分布不同的呢?

我们知道,我们原来使用训练集误差与贝叶斯误差作为可避免偏差,开发集误差与训练集误差作为方差处理,这样可以有效判断到底问题出在方差还是误差上,但分布不同的话,开发集误差与训练集误差的差值可就不仅仅是方差的问题了,可能其还包含了两种数据之间不匹配的问题(就是向两个靶心之间的差距啦,靶心不同肯定会有误差的),那如何评估数据不匹配问题和方差问题呢

我们知道,我们原来开发集与测试集同分布,因而我们不需要考虑数据不匹配的问题,相同地,我们可以找一个和训练集分布相同的来评估下方差问题,故从训练集中抽出一部分数据作为训练-开发集,这部分数据不参与反向传播,这保证了训练-开发集误差与训练集误差之差就是方差,而训练-开发集误差与训练集误差之差就是数据不匹配问题

所以,我们现在需要考虑的就是:

  • 贝叶斯误差(人类误差) ---- 可避免偏差 ---- 训练集误差
  • 训练集误差 ---- 方差 ---- 训练-开发集误差
  • 训练-开发误差 ---- 数据不匹配问题 ---- 开发集误差
  • 开发集误差 ---- 开发集过拟合问题 ---- 测试集误差

有时候会出现一些有趣的事情,比如说开发集与测试集误差比训练集误差小,其实这是可能的事情,比如说上节的语音识别问题,开发集与测试集比训练集更容易识别是完全可能的,我们看下这种情况下要如何进行分析:

我们制作一张二维表格,水平轴上写下各种数据集,比如说是买来的普通语音数据还是收集的车载数据,竖直轴上写下处理数据的不同方法和算法,首先是人类的水平误差,其次是神经网络训练过的数据集上的误差,最后是神经网络未训练过的数据集上的误差

DeepLearning16

(1, 1) 位置自然就是人类水平, (2, 1) 位置自然就是训练集误差, (3, 1) 位置自然就是训练-开发集误差, (3, 2) 位置自然就是开发集误差,他们之间的差值的意义和前面所述一致

而 (1, 2) 位置和 (2, 2) 位置也是有用的, (1, 2) 位置就是人类在车载语音方面的识别误差, (2, 2) 位置就是利用车载语音训练出来的神经网络的误差,我们这样将整个表格填出来之后可以观察到更多的特征,这样就更加方便对神经网络的评估啦

# 3.2.6 Addressing data mismatch

前面提到了数据不匹配这个问题,不过这个问题并没有什么比较有效地解决方案,但我们有一些尝试可以去做一下

首先我们做下错误分析,还是以车载语音助手为例,训练集与开发集之间的差异有可能是汽车的噪音造成的,另外可能发现有很多对街道号识别错误的问题,我们可以从这些问题着手进行优化

比如说噪音问题比较大,那么我们可以在将噪音合成在原数据上,再比如说街道号识别问题比较大,我们可以多增加些人工读数字的数据到训练集

值得注意的是,如果我们使用 1 个小时的车载背景噪音合成在 1w 小时的普通语音数据上会存在一个潜在问题就是,神经网络很可能对这 1 小时的背景音过拟合,虽然我们并不能听出来这一小时背景音是循环使用的,但神经网络可是很容易做到的,所以可以尽量收集更多的背景音以预防这种情况

# 3.2.7 Transfer learning

如果我们有一个已经训练好的图片识别系统,我们已经知道,它比较低的层次实现了对点、线、边缘等的识别功能,所以我们完全可以将这个神经网络迁移到一个放射科图片检测系统中,比如将输出层及其参数删掉,更换新的输出层或者增加几层再输出,然后利用放射科图片对这部分神经网络进行优化,这样可以更快地训练好放射科神经网络,因为神经科图片数据并非像普通图片数据那么容易获得

迁移学习一定要注意,首先输入一定是相同的,这个例子里的输入都是图片,这样保证了前几层的处理效果都是一样的(点线边缘等等),再就是原任务比迁移后的任务数据量大很多,否则的话就失去了迁移学习的意义了

这种学习方法的两次训练之间是串行的,下面介绍并行的学习方法

# 3.2.8 Multi-task learning

比如我们要研究无人驾驶技术,那么我们需要同时检测多种物体,比如说,行人、车辆、停车标记、红绿灯等等,那么我们最终的输出层就类似于 [0, 1, 0, 1],当然这不是 Softmax 分类器,因为这里可能同时出现多个 1 ,所以我们的最终的损失是 1mm=1mj=14L(y^j(i),yj(i))\frac{1}{m} \sum_{m=1}^m \sum_{j=1}^4 L(\hat{y}_j^{(i)}, y_j^{(i)}) ,我们的数据集中可能出现[1, 1, 0, ?] 的情况,也就是没有对是否有红绿灯打标签,那么我们在求和时就忽略那一项

这种方法比之于分成 4 个神经网络分别识别四种物体的好处是,他们共用相同的低层次网络与参数,这样他们会相互学习,使得性能更高,实践也表明了,在神经网络足够大的情况下,这种方法比分开识别的性能更高

当然它也是有适用条件的,首先同时训练的这几组任务可以共用低层次特征,其次,对于某个要专注提升性能的任务,其余所有任务最好超过该任务样本数,最后,前面提到了,多任务是在神经网络比较大的情况下效果更好

通常,人们更多使用学习迁移而较少使用多任务学习,也许因为很难找到那么多相似且数据量对等的任务可以用单一神经网络训练,但他们确实都是比较实用的工具

# 3.2.9 What is end-to-end deep learning?

端对端是指用单个神经网络代替从前多个阶段处理的系统

比如说,我们原来的一个语音识别需要输入语音、提取特征、寻找音位、组成单词最终译成文本,这使得我们很容易获得任务,但如果使用端对端方法,我们可以输入语音、通过一个神经网络、直接输出文本

再举个例子,比如说门禁的人脸识别系统,如果使用端对端的话,应当是输入门禁捕捉到的图片,然后喂入网络识别,但是事实上我们可以分为简单的两步取得更好的效果,首先用软件标记出人脸的位置,然后将人脸裁剪出来并喂入神经网络进行识别,之所以分成两步更加好,因为我们有很多的人脸标记位置的数据与人脸匹配的数据,所以我们可以很方便地建立这两步,但直接一步成型的话,至少现在的效果还是不好的

所以说,端对端并不一定是最好的方法,具体哪个更好我们还需要自己仔细分析下

# 3.2.10 Whether to use end-to-end learning?

是否要使用端对端学习呢?我们先看下它的优缺点:

优点:

  • 更多由数据来说话,没有过多的人为因素干预
  • 人工设计部分更少了

缺点:

  • 不可避免地,我们需要很多很多的数据来训练它
  • 无法采用一些人工设计,因为有些人为干预会有很大的帮助

至于到底要不要用端对端学习,首先我们可能是要看是否有足够的数据来构建端对端学习网络,因为这真的需要很多很多的数据来完成

# 4 Convolutional Neural Networks

# 4.1 Foundations of Convolutional Neural Networks

# 4.1.1 Computer vision

深度学习给计算机视觉带来了极大的突破,计算机视觉中有着不少其他领域可以借鉴的方法

我们之前都是讨论的 (64, 64, 3) 这样的小图,如果我们有 (1000, 1000, 3) 这样的大图呢?直接使用全连接网络的话需要的参数可是……好多好多,所以下面我们就要考虑下卷积操作了

# 4.1.2 Edge detection example

我们之前了解到,神经网络的前几层是对低层次结构进行检测,比如人脸识别神经网络的前几层会检测边缘、区域等等,下面我们看下边缘检测是如何进行的

DeepLearning17

过滤器(卷积核)在原图上滑动,计算出新图,*表示卷积

其实这个卷积处理计算了竖直边缘,为啥呢?看下面这个例子

DeepLearning18

# 4.1.3 More edge detection

  • 竖直边缘检测(左亮右暗为正值)
[101101101] \begin{bmatrix} 1 & 0 & -1\\ 1 & 0 & -1\\ 1 & 0 & -1 \end{bmatrix}
  • 水平边缘检测(上亮下暗为正值)
[111000111] \begin{bmatrix} 1 & 1 & 1\\ 0 & 0 & 0\\ -1 & -1 & -1 \end{bmatrix}
  • Sobel 过滤器
[101202101] \begin{bmatrix} 1 & 0 & -1\\ 2 & 0 & -2\\ 1 & 0 & -1 \end{bmatrix}
  • Scharr 过滤器
[30310010303] \begin{bmatrix} 3 & 0 & -3\\ 10 & 0 & -10\\ 3 & 0 & -3 \end{bmatrix}
  • 你自己的过滤器(作为一个参数,在反向迭代中自动优化)
[w1w2w3w4w5w6w7w8w9] \begin{bmatrix} w_{1} & w_{2} & w_{3}\\ w_{4} & w_{5} & w_{6}\\ w_{7} & w_{8} & w_{9} \end{bmatrix}