神经网络的相关数据表示 - 概念说明
写在前面
数据存储在多维Numpy 数组中,也叫张量(tensor)。一般来说,当前所有机器学习系统都使用张量作为基本数据结构。张量对这个领域非常重要,重要到Google 的 TensorFlow 都以它来命名。那么什么是张量? 张量这一概念的核心在于,它是一个数据容器。它包含的数据几乎总是数值数据,因此它是数字的容器。你可能对矩阵很熟悉,它是二维张量。张量是矩阵向任意维度的推广(这里要特别提一下, 张量的维度(dimension)通常叫作轴(axis))。
张量
标量(0D 张量)
仅包含一个数字的张量叫作标量(scalar,也叫标量张量、零维张量、0D 张量)。在 Numpy 中,一个 float32 或 float64 的数字就是一个标量张量(或标量数组)。你可以用 ndim 属性来查看一个 Numpy 张量的轴的个数。标量张量有 0 个轴(ndim == 0)。张量轴的个数也叫作阶(rank)
。下面是一个 Numpy 标量。
import numpy as np
p = np.array(12)
print(p.ndim)
#output:0
向量(1D 张量)
数字组成的数组叫作向量(vector)或一维张量(1D 张量)。一维张量只有一个轴。下面是 一个 Numpy 向量。
import numpy as np
x = np.array([1, 2, 3, 4, 5, 6])
print(x.ndim)
# output: 1
这个向量有5 个元素,所以被称为5D 向量。不要把5D 向量和5D 张量弄混!5D 向量只 有一个轴,沿着轴有5 个维度,而5D 张量有5 个轴(沿着每个轴可能有任意个维度)。维度 (dimensionality)可以表示沿着某个轴上的元素个数(比如 5D 向量),也可以表示张量中轴的个 数(比如 5D 张量),这有时会令人感到混乱。对于后一种情况,技术上更准确的说法是 5 阶张量 (张量的阶数即轴的个数),但 5D 张量这种模糊的写法更常见。
矩阵(2D 张量)
向量组成的数组叫作矩阵(matrix)或二维张量(2D 张量)。矩阵有 2 个轴(通常叫作行和 列)。你可以将矩阵直观地理解为数字组成的矩形网格。下面是一个 Numpy 矩阵。
import numpy as np
x = np.array([[5, 78, 2, 34, 0],
[6, 79, 3, 35, 1],
[7, 80, 4, 36, 2]])
print(x.ndim)
# output: 2
3D 张量与更高维张量
将多个矩阵组合成一个新的数组,可以得到一个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]]])
print(x.ndim)
# output: 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(以及大多数其他库)中不存在字符串张量,因为张量存储在预先分配的连续内存段中,而字符串的长度是可变的,无法用这种方式存储。
相关概念
-
选 择张量的特定元素叫作
张量切片(tensor slicing)
。 -
通常来说,深度学习中所有数据张量的第一个轴(0 轴,因为索引从0 开始)都是
样本轴 (samples axis,有时也叫样本维度)
。 -
深度学习模型不会同时处理整个数据集,而是将数据拆分成小批量。对于这种批量张量,
第一个轴(0轴)叫作批量轴(batch axis)或批量维度(batch dimension)
。 在使用 Keras 和其他深度学习库时,你会经常遇到这个术语。 -
现实世界中的数据张量
- 向量数据:2D 张量,形状为 (samples, features)。
- 时间序列数据或序列数据:3D 张量,形状为 (samples, timesteps, features)。
- 图像:4D 张量,形状为 (samples, height, width, channels) 或(samples, channels, height, width)。
- 视频:5D 张量,形状为 (samples, frames, height, width, channels) 或(samples, frames, channels, height, width)。
-
向量数据:对于这种数据集,每个数据点都被编码为一个向量,因此一个数据批 量就被编码为 2D 张量(即向量组成的数组),其中第一个轴是
样本轴
,第二个轴是特征轴
。
张量运算
逐元素运算
relu 运算和加法都是逐元素(element-wise)的运算,即该运算独立地应用于张量中的每个元素,也就是说,这些运算非常适合大规模并行实现。如果你想对逐元素运算编写简单的Python 实现,那么可以用 for 循环。下列代码是对逐元素 relu 运算的简单实现。
def naive_relu(x):
assert len(x.shape) == 2
x = x.copy()
for i in range(x.shape[0]):
for j in range(x.shape[1]):
x[i, j] = max(x[i, j], 0)
return x
对于加法采用同样的实现方法。
def naive_add(x, y):
assert len(x.shape) == 2
assert x.shape == y.shape
x = x.copy()
for i in range(x.shape[0]):
for j in range(x.shape[1]):
x[i, j] += y[i, j]
return x
广播
我们将一个2D 张量与一个向量相加,如果将两个形状不同的张量相加,会发生 什么? 如果没有歧义的话,较小的张量会被广播(broadcast),以匹配较大张量的形状。广播包含以下两步。
- 向较小的张量添加轴(叫作广播轴),使其 ndim 与较大的张量相同。
- 将较小的张量沿着新轴重复,使其形状与较大的张量相同。
来看一个具体的例子。假设 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 张量,因为那样做非常低效。下面是一种简单的实现。
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
张量点积
点积运算,也叫张量积(tensor product,不要与逐元素的乘积弄混),是最常见也最有用的 张量运算。与逐元素的运算不同,它将输入张量的元素合并在一起。 在 Numpy、Keras、Theano 和 TensorFlow 中,都是用 * 实现逐元素乘积。TensorFlow 中的 点积使用了不同的语法,但在 Numpy 和 Keras 中,都是用标准的 dot 运算符来实现点积。
import numpy as np
z = np.dot(x, y)
数学符号中的点(.)表示点积运算。
z=x.y
从数学的角度来看,点积运算做了什么?我们首先看一下两个向量 x 和 y 的点积。其计算 过程如下。
import numpy as np
def naive_vector_dot(x, y)
assert len(x.shape) == 1
assert len(y.shape) == 1
assert y.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
assert len(y.shape) == 1
assert x.shape[1] == y.shape[0]
z = np.zeros(x.shape[0])
for i in range(x.shape[0]):
for j in range(y.shape[0]):
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)。
注意,如果两个张量中有一个的 ndim 大于1,那么 dot 运算就不再是对称的,也就是说, dot(x, y) 不等于 dot(y, x)。
更一般地说,你可以对更高维的张量做点积,只要其形状匹配遵循与前面2D 张量相同的 原则:
张量变形
张量变形是指改变张量的行和列,以得到想要的形状。变形后的张量的元素总个数与初始 张量相同。简单的例子可以帮助我们理解张量变形。
x = np.array([[0., 1.],
[2., 3.],
[4., 5.]])
print(x.shape)
#output:(3, 2)
x = x.reshape((6, 1))
print(x)
#output:
#array([[ 0.],
# [ 1.],
# [ 2.],
# [ 3.],
# [ 4.],
# [ 5.]])
经常遇到的一种特殊的张量变形是转置(transposition)。对矩阵做转置是指将行和列互换, 使 x[i, :] 变为 x[:, i]。
x = np.zeros((300, 20))
x = np.transpose(x)
print(x.shape)
#output:(20, 300)
基于梯度的优化
导数中,斜率 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,等等。
梯度
梯度(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 时可以对这样的方程求解,但对于实际的神经网络是 无法求解的,因为参数的个数不会少于几千个,而且经常有上千万个。
基于当前在随机数据批量上的损失,一点一点地对参数进行调节。由于处理的是一个可微函数,你可以计算出它的梯度,从而有效地实现。沿着梯度的反方向更新权重,损失每次都会变小一点。
- 抽取训练样本 x 和对应目标 y 组成的数据批量。
- 在 x 上运行网络,得到预测值 y_pred。
- 计算网络在这批数据上的损失,用于衡量 y_pred 和 y 之间的距离。
- 计算损失相对于网络参数的梯度[一次反向传播(backward pass)]。
- 将参数沿着梯度的反方向移动一点,比如 W -= step * gradient,从而使这批数据 上的损失减小一点。
这很简单!我刚刚描述的方法叫作小批量随机梯度下降(mini-batch stochastic gradient descent, 又称为小批量SGD)
。术语随机(stochastic)是指每批数据都是随机抽取的。
此外,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。也就是说,给定一个运算链,并且已知每个运算的导数,这 些框架就可以利用链式法则来计算这个运算链的梯度函数,将网络参数值映射为梯度值。对于 这样的函数,反向传播就简化为调用这个梯度函数。由于符号微分的出现,你无须手动实现反 向传播算法。因此,我们不会在本节浪费你的时间和精力来推导反向传播的具体公式。你只需 充分理解基于梯度的优化方法的工作原理。