这是深度学习的第二篇~
梯度法
机器学习的主要任务是在学习时寻找最优参数。同样地,神经网络也必须在学习时找到最优参数(权重和偏置)。这里所说的最优参数是指损失函数取最小值时的参数。但是,一般而言,损失函数很复杂,参数空间庞大,我们不知道它在何处能取得最小值。
通过巧妙地使用梯度来寻找函数最小值(或者尽可能小的值)的方法就是梯度法。
需要注意的是,梯度表示的是各点处的函数值减小最多的方向。因此,无法保证梯度所指的方向就是函数的最小值或者真正应该前进的方向。实际上,在复杂的函数中,梯度指示的方向基本上都不是函数值最小处。
虽然梯度的方向并不一定指向最小值,但沿着它的方向能够最大限度地减小函数的值。因此,在寻找函数的最小值(或者尽可能小的值)的位置的任务中,要以梯度的信息为线索,决定前进的方向。梯度法会不断沿梯度方向前进,逐渐减小函数值,从而寻找到尽可能小的值。
用数学式表示梯度法:
上式表示了更新一次的值,这个步骤会在学习中反复执行(上式为两个变量的情况,增多变量的数量式子与之类似)。其中的$\eta$表示更新量,在神经网络的学习中,又称为学习率。
学习率决定在一次学习中,应该学习多少,以及在多大程度上更新参数。学习率需要事先设定好为某个值,一般而言,这个值过大或过小,都无法抵达一个“好的位置”。(像学习率这样的参数称为超参数。这是一种和神经网络的参数(权重和偏置)性质不同的参数,学习率这样的超参数则是人工设定的。一般来说,超参数需要尝试多个值,以便找到一种可以使学习顺利进行的设定)。
用python实现即:1
2
3
4
5
6def gradient_descent(f, init_x, lr=0.01, step_num=100):
x = init_x
for i in (step_num):
grad = numerical_gradient(f, x)
x -= lr * grad
return x
学习算法
神经网络整体学习步骤如下:
- 前提:神经网络存在合适的权重和偏置,调整权重和偏置以便拟合训练数据的过程称为“学习”。神经网络的学习分成下面4个步骤。
- 步骤1(mini-batch):从训练数据中随机选出一部分数据,这部分数据称为mini-batch。我们的目标是减少mini-batch的损失函数的值。
- 步骤2(计算梯度):为了减小mini-batch的损失函数的值,需要求出各个权重参数的梯度。梯度表示损失函数的值减小最多的方向。
- 步骤3(更新参数):将权重参数沿梯度方向进行微小的更新。
- 步骤4(重复):重复步骤1,2,3
神经网络的学习按照上面4个步骤进行。这个方法通过梯度下降法更新参数,不过因为这里使用的数据是随机选择的mini batch数据,所以又称为随机梯度下降法(SGD) 。
假设总数据量为60000,mini-batch的大小为100,则每一次训练中会从60000个训练数据中随机取出100个数据(图像数据和正确解标签数据),然后,对这个包含100笔数据的mini-batch求梯度,使用随机梯度下降法更新参数。在更新过程中,损失函数图像将会出现逐渐下降的趋势:
这意味着学习在正常的进行,表示神经网络的权重参数在逐渐拟合数据。不过这个损失函数的值,严格地讲是“对训练数据的某个mini-batch的损失函数”的值。训练数据的损失函数值减小,虽说是神经网络的学习正常进行的一个信号,但光看这个结果还不能说明该神经网络在其他数据集上也一定能有同等程度的表现。
神经网络的学习中,必须确认是否能够正确识别训练数据以外的其他数据,即确认是否会发生过拟合。所以需要定期地对训练数据和测试数据记录识别精度,一般是每经过一个epoch(一个 epoch 表示学习中所有训练数据均被使用过一次时的更新次数。比如,对于10000笔训练数据,用大小为100笔数据的mini-batch进行学习时,重复随机梯度下降法100次,所有的训练数据就都被“看过”了 。此时,100次就是一个epoch)记录一次。图像大致如下:
实线表示训练数据的识别精度,虚线表示测试数据的识别精度。图中,随epoch的前进,二者识别精度都提高了,且识别精度上基本没差异,则可以说明没有出现过拟合现象。误差反向传播法
先前我们是通过数值微分的方法来计算神经网络的权重参数的梯度,实现虽然简单,但是计算上会费时。我们需要更高效的计算权重参数的梯度,所以需要使用误差反向传播法。计算图
计算图通过节点和箭头表示计算过程,将计算的中间结果写在箭头的上方,表示各个节点的计算结果从左向右传递。以下述问题为示例:
太郎在超市买了2个苹果、3个橘子。其中,苹果每个100日元,橘子每个150日元。消费税是10%,请计算支付金额。
用计算图可以表示为:
如图中从左向右进行计算,即为一种正方向上的传播,简称为正向传播。正向传播是从计算图出发点到结束点的传播。同样的,考虑反向(从图上看的话,就是从右向左)的传播。实际上,这种传播称为反向传播。
计算图的特征是可以通过传递“局部计算”获得最终结果(局部计算是指,无论全局发生了什么,都能只根据与自己相关的信息输出接下来的结果)。
比如超市买了2个苹果和其他很多东西。此时,可以画出如图所示的计算图:
这里的重点是,各个节点处的计算都是局部计算。这意味着,例如苹果和其他很多东西的求和运算并不关心4000这个数字是如何计算而来的,只要把两个数字相加就可以了。也就是说,无论全局的计算多么复杂,各个步骤要做的只是对对象节点的局部计算,通过传递它的计算结果,获得全局的复杂计算的结果。
此外,使用计算图可以通过反向传播高效计算导数。首先得需要理解计算图反向传播的计算,举个使用计算图的反向传播的例子:
反向传播的计算顺序是,将节点输入信号乘以节点的局部导数(偏导数),然后将结果传递给下一个节点。比如,反向传播时,平方运算的节点输入为$\frac{\partial z}{\partial z}$,将其乘上局部导数$\frac{\partial z}{\partial t}$,然后传递给下一个节点。最终传递到最左侧上方结果即为$\frac{\partial z}{\partial z}\frac{\partial z}{\partial t}\frac{\partial t}{\partial x}=\frac{\partial z}{\partial x}$。
基于此运算规则,不难得知,加法的反向传播即是将输入原封不动传给下一节点。
对于乘法,考虑$z=xy$,导数式则为:计算图则如下图:
也不难发现,只需将正向传播的输入信号翻转后相乘即可。
再来考虑购买苹果的例子:
太郎在超市买了2个100日元一个的苹果,消费税是 10%,请计算支付金额,及苹果价格、个数、消费税的上涨会在多大程度上影响最终的支付金额。
这里我们要求的苹果价格的上涨会在多大程度上影响最终的支付金额,即求“支付金额关于苹果的价格的导数“。设苹果的价格为$x$,支付金额为$L$,则相当于求$\frac{\partial L}{\partial x}$。这个导数的值表示当苹果的价格稍微上涨时,支付金额会增加多少。求解则如图:
图中结果可知,苹果的价格的导数是2.2,苹果的个数的导数是110,消费税的导数是200。这可以解释为,如果消费税和苹果的价格增加相同的值,则消费税将对最终价格产生200倍大小的影响,苹果的价格将产生2.2倍大小的影响。这样我们便将导数和计算图联系了起来。
类似的,我们也可以将反向传播法用于神经网络。为方便理解运算,会分开各层分别介绍。激活函数层
激活函数层,实质是叠加了函数$h()$,将输入信号的总和转换为输出信号。所以需对各种不同函数进行讨论。ReLU函数
激活函数ReLU(Rectified Linear Unit )由下式表示:可以得知$y$对$x$的导数:计算图表示为:
用python实现即为:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15class Relu:
def __init__(self):
self.mask = None
def forward(self, x):
self.mask = (x <= 0)
out = x.copy()
out[self.mask] = 0
return out
def backward(self, dout):
dout[self.mask] = 0
dx = dout
return dx
sigmoid函数
sigmoid 函数由下式表示:分步对sigmoid函数计算导数,可表示为如下计算图:
即最终的导数为$\frac{\partial L}{\partial y}y^{2}\exp(-x)$,进一步整理可以得到:最终计算图表示如下:
用python实现即为:1
2
3
4
5
6
7
8
9
10
11
12class Sigmoid:
def __init__(self):
self.out = None
def forward(self, x):
out = 1 / (1 + np.exp(-x))
self.out = out
return out
def backward(self, dout):
dx = dout * (1.0 - self.out) * self.out
return dxAffine层
神经网络的正向传播中,为了计算加权信号的总和,使用了矩阵的乘积运算。用计算图表示:
之前我们见到的计算图中各个节点间流动的是标量,而该计算图中各个节点间传播的是矩阵。以矩阵为对象的反向传播,按矩阵的各个元素进行计算时,步骤和以标量为对象的计算图相同。
以下图标注为例:
矩阵的乘积(“dot”节点)的反向传播可以通过组建使矩阵对应维度的元素个数一致的乘积运算而推导出来。可以得到以下结论:推导过程可以参考:https://zhuanlan.zhihu.com/p/628063109批版本Affine层
前面介绍的Affine层的输入$\mathbf{X}$是以单个数据为对象的。现在我们考虑N个数据一起进行正向传播的情况(也就是$\mathbf{X}$从向量变为了矩阵形式),也就是批版本的Affine层。先给出批版本的Affine层的计算图,如图所示:
可以发现和前面是一样的,唯一需要注意的是偏置$\mathbf{B}$会加到$\mathbf{X}\cdot \mathbf{W}$各个数据上(也就是把向量$\mathbf{B}$变为N行的矩阵,再进行相加)。
用python实现即为:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18class Affine:
def __init__(self, W, b):
self.W = W
self.b = b
self.x = None
self.dW = None
self.db = None
def forward(self, x):
self.x = x
out = np.dot(x, self.W) + self.b
return out
def backward(self, dout):
dx = np.dot(dout, self.W.T)
self.dW = np.dot(self.x.T, dout)
self.db = np.sum(dout, axis=0)
return dxsoftmax-with-loss层
前面我们提到过,softmax函数会将输入值正规化之后再输出。比如手写数字识别时,Softmax层的输出如图所示:
在图中,Softmax 层将输入值正规化(将输出值的和调整为1)之后再输出。另外,因为手写数字识别要进行10类分类,所以向Softmax层的输入也有10个。
%% 神经网络中进行的处理有推理和学习两个阶段。神经网络的推理通常不使用Softmax层。比如,用图中的网络进行推理时,会将最后一个Affine层的输出作为识别结果。神经网络中未被正规化的输出结果(图中Softmax层前面的Affine层的输出)有时被称为“得分”。也就是说,当神经网络的推理只需要给出一个答案的情况下,因为此时只对得分最大值感兴趣,所以不需要Softmax层。不过,神经网络的学习阶段则需要Softmax层。 %%
下面来实现Softmax层。考虑到这里也包含作为损失函数的交叉熵误差(cross entropy error),所以称为“Softmax-with-Loss层”。Softmax-with-Loss层(Softmax函数和交叉熵误差)的计算图如图所示。
可以看到,Softmax-with-Loss层有些复杂。下图中,softmax 函数记为Softmax层,交叉熵误差记为Cross Entropy Error 层。这里假设要进行3类分类,从前面的层接收3个输入(得分)。如图所示,Softmax层将输入$(a_{1}, a_{2}, a_{3})$正规化,输出$(y_{1}, y_{2}, y_{3})$。Cross Entropy Error层接收Softmax的输出$(y_{1}, y_{2}, y_{3})$和教师标签$(t_{1}, t_{2}, t_{3)}$,从这些数据中输出损失L。
要注意的是反向传播的结果。Softmax层的反向传播得到了$(y_{1}−t_{1}, y_{2}−t_{2}, y_{3}− t_{3})$这样“漂亮”的结果。由于$(y_{1}, y_{2}, y_{3})$是 Softmax层的输出, $(t_{1}, t_{2}, t_{3})$ 是监督数据,所以 $(y_{1}− t_{1}, y_{2}− t_{2}, y_{3}− t_{3})$是Softmax层的输出和教师标签的差分。神经网络的反向传播会把这个差分表示的误差传递给前面的层,这是神经网络学习中的重要性质。
神经网络学习的目的就是通过调整权重参数,使神经网络的输出(Softmax的输出)接近教师标签。因此,必须将神经网络的输出与教师标签的误差高效地传递给前面的层。刚刚的$(y_{1}− t_{1}, y_{2}− t_{2}, y_{3}− t_{3})$正是 Softmax 层的输出与教师标签的差,直截了当地表示了当前神经网络的输出与教师标签的误差。
python实现:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16class SoftmaxWithLoss:
def __init__(self):
self.loss = None # 损失
self.y = None # softmax的输出
self.t = None # 监督数据( one-hot vector)
def forward(self, x, t):
self.t = t
self.y = softmax(x)
self.loss = cross_entropy_error(self.y, self.t)
return self.loss
def backward(self, dout=1):
batch_size = self.t.shape[0]
dx = (self.y - self.t) / batch_size
return dx