在机器学习中,分类问题中的某个类别叫作类(class)。数据点叫作样本(sample)。某个样本对应的类叫作标签(label)。
mnist数据集是机器学习领域的一个经典数据集,这个数据集包含 60 000 张训练图像和 10 000 张测试图,将手写数字的灰度图像(28 像素×28 像素)划分到 10 个类别中(0~9)。
首先,将训练数据(train_images 和 train_labels)输入神经网络;其次,网络学习将图像和标签关联在一起;最后,网络对 test_images 生成预测,而我们将验证这些预测与 test_labels 中的标签是否匹配。
from keras.datasets import mnist
(train_images, train_labels), (test_images, test_labels) = mnist.load_data()
train_images 和 train_labels 组成了训练集(training set),模型将从这些数据中进行 学习。然后在测试集(test set,即 test_images 和 test_labels)上对模型进行测试。
图像被编码为 Numpy 数组,而标签是数字数组,取值范围为 0~9。图像和标签一一对应。
train_images = train_images.reshape((60000, 28 * 28))
train_images = train_images.astype('float32') / 255
test_images = test_images.reshape((10000, 28 * 28))
test_images = test_images.astype('float32') / 255
在开始训练之前,我们将对数据进行预处理,将其变换为网络要求的形状,并缩放到所有值都在 [0, 1] 区间。比如,之前训练图像保存在一个 uint8 类型的数组中,其形状为(60000, 28, 28),取值区间为 [0, 255]。我们需要将其变换为一个 float32 数组,其形状为 (60000, 28 * 28),取值范围为 0~1。
from keras.utils import to_categorical
train_labels = to_categorical(train_labels)
test_labels = to_categorical(test_labels)
from keras import models
from keras import layers
network = models.Sequential()
network.add(layers.Dense(512, activation='relu', input_shape=(28 * 28,)))
network.add(layers.Dense(10, activation='softmax'))
神经网络的核心组件是层(layer),它是一种数据处理模块,你可以将它看成数据过滤器。进去一些数据,出来的数据变得更加有用。具体来说,层从输入数据中提取表示——我们期望这种表示有助于解决手头的问题。大多数深度学习都是将简单的层链接起来,从而实现渐进式的数据蒸馏(data distillation)。深度学习模型就像是数据处理的筛子,包含一系列越来越精细的
数据过滤器(即层)。
本例中的网络包含 2 个 Dense 层,它们是密集连接(也叫全连接)的神经层。第二层(也是最后一层)是一个 10 路 softmax 层,它将返回一个由 10 个概率值(总和为 1)组成的数组。每个概率值表示当前数字图像属于 10 个数字类别中某一个的概率。
network.compile(optimizer='rmsprop', # 优化器
loss='categorical_crossentropy', # 损失函数
metrics=['accuracy']) # 在训练和测试过程中需要监控的指标
network.fit(train_images, train_labels, epochs=5, batch_size=128)
Keras 中这一步是通过调用网络的 fit 方法来完成的——我们在训练数据上拟合(fit)模型
test_loss, test_acc = network.evaluate(test_images, test_labels)
print('test_acc:', test_acc)
前面例子使用的数据存储在多维 Numpy 数组中,也叫张量(tensor)。
张量这一概念的核心在于,它是一个数据容器。它包含的数据几乎总是数值数据,因此它是数字的容器。矩阵是二维张量。张量是矩阵向任意维度的推广[注意,张量的维度(dimension)通常叫作轴(axis)]。
仅包含一个数字的张量叫作标量(scalar,也叫标量张量、零维张量、0D 张量)。在 Numpy中,一个 float32 或 float64 的数字就是一个标量张量(或标量数组)。
可以用 ndim 属性来查看一个 Numpy 张量的轴的个数。标量张量有 0 个轴(ndim == 0)。张量轴的个数也叫作阶(rank)。下面是一个 Numpy 标量。
>>> import numpy as np
>>> x = np.array(12)
>>> x
array(12)
>>> x.ndim
0
数字组成的数组叫作向量(vector)或一维张量(1D 张量)。一维张量只有一个轴。下面是一个 Numpy 向量。
>>> x = np.array([12, 3, 6, 14, 7])
>>> x
array([12, 3, 6, 14, 7])
>>> x.ndim
1
这个向量有 5 个元素,所以被称为 5D 向量。不要把 5D 向量和 5D 张量弄混! 5D 向量只 有一个轴,沿着轴有 5 个维度,而 5D 张量有 5 个轴(沿着每个轴可能有任意个维度)。
维度(dimensionality)可以表示沿着某个轴上的元素个数(比如 5D 向量),也可以表示张量中轴的个数(比如 5D 张量),这有时会令人感到混乱。对于后一种情况,技术上更准确的说法是 5 阶张量(张量的阶数即轴的个数),但 5D 张量这种模糊的写法更常见。
向量组成的数组叫作矩阵(matrix)或二维张量(2D 张量)。矩阵有 2 个轴(通常叫作行和列)。你可以将矩阵直观地理解为数字组成的矩形网格。下面是一个 Numpy 矩阵。
>>> x = np.array([[5, 78, 2, 34, 0],
[6, 79, 3, 35, 1],
[7, 80, 4, 36, 2]])
>>> x.ndim
2
第一个轴上的元素叫作行(row),第二个轴上的元素叫作列(column)。在上面的例子中,[5, 78, 2, 34, 0] 是 x 的第一行,[5, 6, 7] 是第一列。
将多个矩阵组合成一个新的数组,可以得到一个 3D 张量,你可以将其直观地理解为数字组成的立方体。下面是一个 Numpy 的 3D 张量。
>>> x = np.array([[[5, 78, 2, 34, 0],
[6, 79, 3, 35, 1],
[7, 80, 4, 36, 2]],
[[5, 78, 2, 34, 0],
[6, 79, 3, 35, 1],
[7, 80, 4, 36, 2]],
[[5, 78, 2, 34, 0],
[6, 79, 3, 35, 1],
[7, 80, 4, 36, 2]]])
>>> x.ndim
3
将多个 3D 张量组合成一个数组,可以创建一个 4D 张量,以此类推。深度学习处理的一般是 0D 到 4D 的张量,但处理视频数据时可能会遇到 5D 张量。
张量是由以下三个关键属性来定义的。
轴的个数(阶):例如,3D 张量有 3 个轴,矩阵有 2 个轴。这在 Numpy 等 Python 库中也叫张量的 ndim。
形状:这是一个整数元组,表示张量沿每个轴的维度大小(元素个数)。例如,前面矩阵示例的形状为 (3, 5),3D 张量示例的形状为 (3, 3, 5)。向量的形状只包含一个元素,比如 (5,),而标量的形状为空,即 ()。
数据类型(在 Python 库中通常叫作 dtype):这是张量中所包含数据的类型,例如,张量的类型可以是 float32、uint8、float64 等。在极少数情况下,你可能会遇到字符(char)张量。注意Numpy(以及大多数其他库)中不存在字符串张量,因为张量存储在预先分配的连续内存段中,而字符串的长度是可变的,无法用这种方式存储。
使用语法 train_images[i] 来选择沿着第一个轴的特定数字。选择张量的特定元素叫作张量切片(tensor slicing)。
>>> my_slice = train_images[10:100] # 选择第 10~100 个数字(不包括第 100 个)
>>> print(my_slice.shape)
(90, 28, 28)
# 写法2
>>> my_slice = train_images[10:100, :, :]
>>> my_slice.shape
(90, 28, 28)
# 写法3
>>> my_slice = train_images[10:100, 0:28, 0:28]
>>> my_slice.shape
(90, 28, 28)
通常来说,深度学习中所有数据张量的第一个轴(0 轴,因为索引从 0 开始)都是样本轴(samples axis,有时也叫样本维度)。在 MNIST 的例子中,样本就是数字图像。此外,深度学习模型不会同时处理整个数据集,而是将数据拆分成小批量。具体来看,下面是 MNIST 数据集的一个批量,批量大小为 128。
batch = train_images[:128]
然后是下一个批量。
batch = train_images[128:256]
然后是第 n 个批量。
batch = train_images[128 * n:128 * (n + 1)]
对于这种批量张量,第一个轴(0 轴)叫作批量轴(batch axis)或批量维度(batch dimension)。
2D 张量,形状为 (samples, features)
这是最常见的数据。对于这种数据集,每个数据点都被编码为一个向量,因此一个数据批量就被编码为 2D 张量(即向量组成的数组),其中第一个轴是样本轴,第二个轴是特征轴。
例如:人口统计数据集、文本文档数据集
3D 张量,形状为 (samples, timesteps, features)。
当时间(或序列顺序)对于数据很重要时,应该将数据存储在带有时间轴的 3D 张量中。每个样本可以被编码为一个向量序列(即 2D 张量),因此一个数据批量就被编码为一个 3D 张量。
例如:股票价格数据集、推文数据集
4D 张量,形状为 (samples, height, width, channels) 或 (samples, channels, height, width)。
图像通常具有三个维度:高度、宽度和颜色深度。虽然灰度图像(比如 MNIST 数字图像)只有一个颜色通道,因此可以保存在 2D 张量中,但按照惯例,图像张量始终都是 3D 张量,灰度图像的彩色通道只有一维。因此,如果图像大小为 256×256,那么 128 张灰度图像组成的批量可以保存在一个形状为 (128, 256, 256, 1) 的张量中,而 128 张彩色图像组成的批量则可以保存在一个形状为 (128, 256, 256, 3) 的张量中。
图像张量的形状有两种约定:通道在后(channels-last)的约定(在 TensorFlow 中使用)和通道在前(channels-first)的约定(在 Theano 中使用)。Google 的 TensorFlow 机器学习框架将颜色深度轴放在最后:(samples, height, width, color_depth)。与此相反,Theano将图像深度轴放在批量轴之后:(samples, color_depth, height, width)。如果采用 Theano 约定,前面的两个例子将变成 (128, 1, 256, 256) 和 (128, 3, 256, 256)。Keras 框架同时支持这两种格式。
5D 张量,形状为 (samples, frames, height, width, channels) 或 (samples, frames, channels, height, width)。
视频数据是现实生活中需要用到 5D 张量的少数数据类型之一。视频可以看作一系列帧,每一帧都是一张彩色图像。由于每一帧都可以保存在一个形状为 (height, width, color_depth) 的 3D 张量中,因此一系列帧可以保存在一个形状为 (frames, height, width, color_depth) 的 4D 张量中,而不同视频组成的批量则可以保存在一个 5D 张量中,其形状为(samples, frames, height, width, color_depth)。
在最开始的例子中,我们通过叠加 Dense 层来构建网络。Keras 层的实例如下所示。
keras.layers.Dense(512, activation=‘relu’)
这个层可以理解为一个函数,输入一个 2D 张量,返回另一个 2D 张量,即输入张量的新表示。具体而言,这个函数如下所示(其中 W 是一个 2D 张量,b 是一个向量,二者都是该层的属性)。
output = relu(dot(W, input) + b)
我们将上式拆开来看。这里有三个张量运算:输入张量和张量 W 之间的点积运算(dot)、得到的 2D 张量与向量 b 之间的加法运算(+)、最后的 relu 运算。relu(x) 是 max(x, 0)。
relu 运算和加法都是逐元素(element-wise)的运算,即该运算独立地应用于张量中的每个元素,也就是说,这些运算非常适合大规模并行实现。
对逐元素运算编写简单的 Python 实现,那么可以用 for 循环。下列代码是对逐元素 relu 运算的简单实现。
def naive_relu(x):
assert len(x.shape) == 2 # x 是一个 Numpy 的 2D 张量
x = x.copy() # x 是一个 Numpy 的 2D 张量
for i in range(x.shape[0]):
for j in range(x.shape[1]):
x[i, j] = max(x[i, j], 0)
return x
根据同样的方法,可以实现逐元素的加法、乘法、减法等。
在实践中处理 Numpy 数组时,这些运算都是优化好的 Numpy 内置函数。因此,在 Numpy 中可以直接进行下列逐元素运算,速度非常快
import numpy as np
z = x + y # 逐元素的相加
z = np.maximum(z, 0.) # 逐元素的 relu
将一个 2D 张量与一个向量相加。如果将两个形状不同的张量相加,较小的张量会被广播(broadcast),以匹配较大张量的形状。
广播的两步
(1) 向较小的张量添加轴(叫作广播轴),使其 ndim 与较大的张量相同。
(2) 将较小的张量沿着新轴重复,使其形状与较大的张量相同。
来看一个具体的例子。假设 X 的形状是 (32, 10),y 的形状是 (10,)。首先,我们给 y添加空的第一个轴,这样 y 的形状变为 (1, 10)。然后,我们将 y 沿着新轴重复 32 次,这样得到的张量 Y 的形状为 (32, 10),并且 Y[i, :] == y for i in range(0, 32)。现在,我们可以将 X 和 Y 相加,因为它们的形状相同。
在实际的实现过程中并不会创建新的 2D 张量,因为那样做非常低效。重复的操作完全是虚拟的,它只出现在算法中,而没有发生在内存中。但想象将向量沿着新轴重复 10 次,是一种很有用的思维模型。下面是一种简单的实现。
def naive_add_matrix_and_vector(x, y):
assert len(x.shape) == 2
assert len(y.shape) == 1
assert x.shape[1] == y.shape[0]
x = x.copy()
for i in range(x.shape[0]):
for j in range(x.shape[1]):
x[i, j] += y[j]
return x
如果一个张量的形状是 (a, b, … n, n+1, … m),另一个张量的形状是 (n, n+1, … m),那么你通常可以利用广播对它们做两个张量之间的逐元素运算。广播操作会自动应用于从 a 到 n-1 的轴。
下面这个例子利用广播将逐元素的 maximum 运算应用于两个形状不同的张量。
import numpy as np
x = np.random.random((64, 3, 32, 10)) # x 是形状为 (64, 3, 32, 10) 的随机张量
y = np.random.random((32, 10)) # y 是形状为 (32, 10) 的随机张量
z = np.maximum(x, y) # 输出 z 的形状是 (64, 3, 32, 10),与 x 相同
点积运算,也叫张量积(tensor product,不要与逐元素的乘积弄混),是最常见也最有用的张量运算。与逐元素的运算不同,它将输入张量的元素合并在一起。
在 Numpy、Keras、Theano 和 TensorFlow 中,都是用 * 实现逐元素乘积。TensorFlow 中的点积使用了不同的语法,但在 Numpy 和 Keras 中,都是用标准的 dot 运算符来实现点积。
import numpy as np
z = np.dot(x, y)
数学符号中的点(.)表示点积运算 z=x.y
从数学的角度来看,看一下两个向量 x 和 y 的点积。其计算过程如下
def naive_vector_dot(x, y):
assert len(x.shape) == 1
assert len(y.shape) == 1
assert x.shape[0] == y.shape[0]
z = 0.
for i in range(x.shape[0]):
z += x[i] * y[i]
return z
注意,两个向量之间的点积是一个标量,而且只有元素个数相同的向量之间才能做点积。
你还可以对一个矩阵 x 和一个向量 y 做点积,返回值是一个向量,其中每个元素是 y 和 x的每一行之间的点积。其实现过程如下。
import numpy as np
def naive_matrix_vector_dot(x, y):
assert len(x.shape) == 2 # x 是一个 Numpy 矩阵
assert len(y.shape) == 1 # y 是一个 Numpy 向量
assert x.shape[1] == y.shape[0] # x 的第 1 维和 y 的第 0 维大小必须相同
z = np.zeros(x.shape[0])
for i in range(x.shape[0]):
for j in range(x.shape[1]):
z[i] += x[i, j] * y[j]
return z
可以复用前面写过的代码,从中可以看出矩阵 - 向量点积与向量点积之间的关系。
def naive_matrix_vector_dot(x, y):
z = np.zeros(x.shape[0])
for i in range(x.shape[0]):
z[i] = naive_vector_dot(x[i, :], y)
return z
注意,如果两个张量中有一个的 ndim 大于 1,那么 dot 运算就不再是对称的,也就是说,dot(x, y) 不等于 dot(y, x)。
当然,点积可以推广到具有任意个轴的张量。最常见的应用可能就是两个矩阵之间的点积。对于两个矩阵 x 和 y,当且仅当 x.shape[1] == y.shape[0] 时,你才可以对它们做点积(dot(x, y))。得到的结果是一个形状为 (x.shape[0], y.shape[1]) 的矩阵,其元素为 x的行与 y 的列之间的点积。其简单实现如下。
def naive_matrix_dot(x, y):
assert len(x.shape) == 2
assert len(y.shape) == 2
assert x.shape[1] == y.shape[0]
z = np.zeros((x.shape[0], y.shape[1]))
for i in range(x.shape[0]):
for j in range(y.shape[1]):
row_x = x[i, :]
column_y = y[:, j]
z[i, j] = naive_vector_dot(row_x, column_y)
return z
更一般地说,你可以对更高维的张量做点积,只要其形状匹配遵循与前面 2D 张量相同的原则:
(a, b, c, d) . (d,) -> (a, b, c)
(a, b, c, d) . (d, e) -> (a, b, c, e)
第三个重要的张量运算是张量变形(tensor reshaping)。虽然前面神经网络第一个例子的Dense 层中没有用到它,但在将图像数据输入神经网络之前,我们在预处理时用到了这个运算。
张量变形是指改变张量的行和列,以得到想要的形状。变形后的张量的元素总个数与初始张量相同。简单的例子可以帮助我们理解张量变形。
>>> x = np.array([[0., 1.],
[2., 3.],
[4., 5.]])
>>> print(x.shape)
(3, 2)
>>> x = x.reshape((6, 1))
>>> x
array([[ 0.],
[ 1.],
[ 2.],
[ 3.],
[ 4.],
[ 5.]])
>>> x = x.reshape((2, 3))
>>> x
array([[ 0., 1., 2.],
[ 3., 4., 5.]])
经常遇到的一种特殊的张量变形是转置(transposition)。对矩阵做转置是指将行和列互换,使 x[i, :] 变为 x[:, i]。
>>> x = np.zeros((300, 20)) # 创建一个形状为 (300, 20) 的零矩阵
>>> x = np.transpose(x)
>>> print(x.shape)
(20, 300)
对于张量运算所操作的张量,其元素可以被解释为某种几何空间内点的坐标,因此所有的张量运算都有几何解释。举个例子,我们来看加法。首先有这样一个向量:A = [0.5, 1]
它是二维空间中的一个点。常见的做法是将向量描绘成原点到这个点的箭头
假设又有一个点:B = [1, 0.25],将它与前面的 A 相加。从几何上来看,这相当于将两个向量箭头连在一起,得到的位置表示两个向量之和对应的向量
通常来说,仿射变换、旋转、缩放等基本的几何操作都可以表示为张量运算。举个例子,要将一个二维向量旋转 theta 角,可以通过与一个 2×2 矩阵做点积来实现,这个矩阵为 R = [u, v],其中 u 和 v 都是平面向量:u = [cos(theta), sin(theta)],v = [-sin(theta), cos(theta)]。
前面讲过,神经网络完全由一系列张量运算组成,而这些张量运算都只是输入数据的几何变换。因此,可以将神经网络解释为高维空间中非常复杂的几何变换,这种变换可以通过许多简单的步骤来实现。
更新网络的所有权重一种好的方法是利用网络中所有运算都是可微(differentiable)的这一事实,计算损失相对于网络系数的梯度(gradient),然后向梯度的反方向改变系数,从而使损失降低。
斜率 a 被称为 f 在 p 点的导数(derivative)。如果 a 是负的,说明 x 在 p 点附近的微小变化将导致 f(x) 减小(如图 2-10 所示);如果 a 是正的,那么 x 的微小变化将导致 f(x) 增大。此外,a 的绝对值(导数大小)表示增大或减小的速度快慢。
对于每个可微函数 f(x)(可微的意思是“可以被求导”。例如,光滑的连续函数可以被求导),都存在一个导数函数 f’(x),将 x 的值映射为 f 在该点的局部线性近似的斜率。例如,cos(x)的导数是 -sin(x),f(x) = a * x 的导数是 f’(x) = a,等等。
如果你想要将 x 改变一个小因子 epsilon_x,目的是将 f(x) 最小化,并且知道 f 的导数,那么问题解决了:导数完全描述了改变 x 后 f(x) 会如何变化。如果你希望减小 f(x) 的值,只需将 x 沿着导数的反方向移动一小步。
梯度(gradient)是张量运算的导数。它是导数这一概念向多元函数导数的推广。多元函数是以张量作为输入的函数。
假设有一个输入向量 x、一个矩阵 W、一个目标 y 和一个损失函数 loss。你可以用 W 来计算预测值 y_pred,然后计算损失,或者说预测值 y_pred 和目标 y 之间的距离。
y_pred = dot(W, x)
loss_value = loss(y_pred, y)
如果输入数据 x 和 y 保持不变,那么这可以看作将 W 映射到损失值的函数。
loss_value = f(W)
假设 W 的当前值为 W0。f 在 W0 点的导数是一个张量 gradient(f)(W0),其形状与 W 相同,每个系数 gradient(f)(W0)[i, j] 表示改变 W0[i, j] 时 loss_value 变化的方向和大小。张量 gradient(f)(W0) 是函数 f(W) = loss_value 在 W0 的导数。
前面已经看到,单变量函数 f(x) 的导数可以看作函数 f 曲线的斜率。同样,gradient(f)(W0) 也可以看作表示 f(W) 在 W0 附近曲率(curvature)的张量。
对于一个函数 f(x),你可以通过将 x 向导数的反方向移动一小步来减小 f(x) 的值。同样,对于张量的函数 f(W),你也可以通过将 W 向梯度的反方向移动来减小 f(W),比如 W1 = W0 - step * gradient(f)(W0),其中 step 是一个很小的比例因子。也就是说,沿着曲率的反方向移动,直观上来看在曲线上的位置会更低。注意,比例因子 step 是必需的,因为gradient(f)(W0) 只是 W0 附近曲率的近似值,不能离 W0 太远。
给定一个可微函数,理论上可以用解析法找到它的最小值:函数的最小值是导数为 0 的点,因此你只需找到所有导数为 0 的点,然后计算函数在其中哪个点具有最小值。
将这一方法应用于神经网络,就是用解析法求出最小损失函数对应的所有权重值。可以通过对方程 gradient(f)(W) = 0 求解 W 来实现这一方法。这是包含 N 个变量的多项式方程,其中 N 是网络中系数的个数。N=2 或 N=3 时可以对这样的方程求解,但对于实际的神经网络是无法求解的,因为参数的个数不会少于几千个,而且经常有上千万个。
基于当前在随机数据批量上的损失,一点一点地对参数进行调节。由于处理的是一个可微函数,你可以计算出它的梯度,从而有效地实现第四步。沿着梯度的反方向更新权重,损失每次都会变小一点。
(1) 抽取训练样本 x 和对应目标 y 组成的数据批量。
(2) 在 x 上运行网络,得到预测值 y_pred。
(3) 计算网络在这批数据上的损失,用于衡量 y_pred 和 y 之间的距离。
(4) 计算损失相对于网络参数的梯度[一次反向传播(backward pass)]。
(5) 将参数沿着梯度的反方向移动一点,比如 W -= step * gradient,从而使这批数据上的损失减小一点。
这个方法叫作小批量随机梯度下降(mini-batch stochastic gradient descent,又称为小批量 SGD)。术语随机(stochastic)是指每批数据都是随机抽取的(stochastic 是 random在科学上的同义词 )。下图给出了一维的情况,网络只有一个参数,并且只有一个训练样本。
直观上来看,为 step 因子选取合适的值是很重要的。如果取值太小,则沿着曲线的下降需要很多次迭代,而且可能会陷入局部极小点。如果取值太大,则更新权重值之后可能会出现在曲线上完全随机的位置。
注意,小批量 SGD 算法的一个变体是每次迭代时只抽取一个样本和目标,而不是抽取一批数据。这叫作真 SGD(有别于小批量 SGD)。还有另一种极端,每一次迭代都在所有数据上运行,这叫作批量 SGD。这样做的话,每次更新都更加准确,但计算代价也高得多。这两个极端之间的有效折中则是选择合理的批量大小。
此外,SGD 还有多种变体,其区别在于计算下一次权重更新时还要考虑上一次权重更新,而不是仅仅考虑当前梯度值,比如带动量的 SGD、Adagrad、RMSProp 等变体。这些变体被称为优化方法(optimization method)或优化器(optimizer)。其中动量的概念尤其值得关注,它在许多变体中都有应用。动量解决了 SGD 的两个问题:收敛速度和局部极小点。
在某个参数值附近,有一个局部极小点(local minimum):在这个点附近,向左移动和向右移动都会导致损失值增大。如果使用小学习率的 SGD 进行优化,那么优化过程可能会陷入局部极小点,导致无法找到全局最小点。
使用动量方法可以避免这样的问题,这一方法的灵感来源于物理学。有一种有用的思维图像,就是将优化过程想象成一个小球从损失函数曲线上滚下来。如果小球的动量足够大,那么它不会卡在峡谷里,最终会到达全局最小点。动量方法的实现过程是每一步都移动小球,不仅要考虑当前的斜率值(当前的加速度),还要考虑当前的速度(来自于之前的加速度)。这在实践中的是指,更新参数 w 不仅要考虑当前的梯度值,还要考虑上一次的参数更新,其简单实现如下所示。
past_velocity = 0.
momentum = 0.1 # 不变的动量因子
while loss > 0.01: # 优化循环
w, loss, gradient = get_current_parameters()
velocity = past_velocity * momentum - learning_rate * gradient
w = w + momentum * velocity - learning_rate * gradient
past_velocity = velocity
update_parameter(w)
在前面的算法中,我们假设函数是可微的,因此可以明确计算其导数。在实践中,神经网络函数包含许多连接在一起的张量运算,每个运算都有简单的、已知的导数。例如,下面这个网络 f 包含 3 个张量运算 a、b 和 c,还有 3 个权重矩阵 W1、W2 和 W3。
f(W1, W2, W3) = a(W1, b(W2, c(W3)))
根据微积分的知识,这种函数链可以利用下面这个恒等式进行求导,它称为链式法则(chain rule):(f(g(x)))’ = f’(g(x)) * g’(x)。将链式法则应用于神经网络梯度值的计算,得到的算法叫作反向传播(backpropagation,有时也叫反式微分,reverse-mode differentiation)。反向传播从最终损失值开始,从最顶层反向作用至最底层,利用链式法则计算每个参数对损失值的贡献大小。
现在以及未来数年,人们将使用能够进行符号微分(symbolic differentiation)的现代框架来实现神经网络,比如 TensorFlow。也就是说,给定一个运算链,并且已知每个运算的导数,这些框架就可以利用链式法则来计算这个运算链的梯度函数,将网络参数值映射为梯度值。对于这样的函数,反向传播就简化为调用这个梯度函数。由于符号微分的出现,你无须手动实现反向传播算法。因此,我们不会在本节浪费你的时间和精力来推导反向传播的具体公式。你只需充分理解基于梯度的优化方法的工作原理。