标量(0D 张量)
仅包含一个数字的张量叫作标量(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
向量(1D 张量)
数字组成的数组叫作向量(vector)或一维张量(1D 张量)。一维张量只有一个轴。下面是
一个Numpy 向量。
>>> x = np.array([12, 3, 6, 14, 7]) >>> x array([12, 3, 6, 14, 7]) >>> x.ndim 1
矩阵(2D 张量)
向量组成的数组叫作矩阵(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 张量与更高维张量
将多个矩阵组合成一个新的数组,可以得到一个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 张量有 3 个轴,矩阵有 2 个轴。这在 Numpy 等 Python 库中
也叫张量的ndim。
形状。这是一个整数元组,表示张量沿每个轴的维度大小(元素个数)。例如,前面矩
阵示例的形状为(3, 5),3D 张量示例的形状为(3, 3, 5)。向量的形状只包含一个
元素,比如(5,),而标量的形状为空,即()。
数据类型(在 Python 库中通常叫作 dtype)。这是张量中所包含数据的类型,例如,张
量的类型可以是float32、uint8、float64 等。在极少数情况下,你可能会遇到字符
(char)张量。注意,Numpy(以及大多数其他库)中不存在字符串张量,因为张量存
储在预先分配的连续内存段中,而字符串的长度是可变的,无法用这种方式存储。
以MNIST 例子说明
张量train_images 的轴的个数,即ndim 属性
>>> print(train_images.ndim)
3
下面是它的形状
>>> print(train_images.shape)
(60000, 28, 28)
下面是它的数据类型,即dtype 属性
>>> print(train_images.dtype)
uint8
所以,这里train_images 是一个由8 位整数组成的3D 张量。更确切地说,它是60 000
个矩阵组成的数组,每个矩阵由28×28 个整数组成。每个这样的矩阵都是一张灰度图像,元素
取值范围为0~255。
在Numpy 中操作张量
选择张量的特定元素叫作张量切片(tensor slicing)
选择第10~100 个数字(不包括第100 个),并将其放在形状为(90, 28,28) 的数组中。
>>> my_slice = train_images[10:100]
>>> print(my_slice.shape)
(90, 28, 28)
它等同于下面这个更复杂的写法,给出了切片沿着每个张量轴的起始索引和结束索引。
注意,: 等同于选择整个轴。
>>> my_slice = train_images[10:100, :, :] #等同于前面的例子
>>> my_slice.shape
(90, 28, 28)
>>> my_slice = train_images[10:100, 0:28, 0:28] #也等同于前面的例子
>>> my_slice.shape
(90, 28, 28)
一般来说,你可以沿着每个张量轴在任意两个索引之间进行选择。例如,你可以在所有图
像的右下角选出14 像素×14 像素的区域:
my_slice = train_images[:, 14:, 14:]
也可以使用负数索引。与Python 列表中的负数索引类似,它表示与当前轴终点的相对位置。
你可以在图像中心裁剪出14 像素×14 像素的区域:
my_slice = train_images[:, 7:-7, 7:-7]
现实世界中的数据张量
向量数据: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)
广播
如果将两个形状不同的张量相加,会发生
什么?
如果没有歧义的话,较小的张量会被广播(broadcast),以匹配较大张量的形状。广播包含
以下两步。
(1) 向较小的张量添加轴(叫作广播轴),使其ndim 与较大的张量相同。
(2) 将较小的张量沿着新轴重复,使其形状与较大的张量相同。
张量点积
在Numpy 和Keras 中,都是用标准的dot 运算符来实现点积。
import numpy as np
z = np.dot(x, y)
对于两个矩阵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
为了便于理解点积的形状匹配,可以利用可视化来帮助理解。
张量变形(tensor reshaping)
张量变形是指改变张量的行和列,以得到想要的形状。变形后的张量的元素总个数与初始
张量相同。简单的例子可以帮助我们理解张量变形。
>>> 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)) >>> x = np.transpose(x) >>> print(x.shape) (20, 300)
深度学习的几何解释
前面讲过,神经网络完全由一系列张量运算组成,而这些张量运算都只是输入数据的几何
变换。因此,你可以将神经网络解释为高维空间中非常复杂的几何变换,这种变换可以通过许
多简单的步骤来实现。
对于三维的情况,下面这个思维图像是很有用的。想象有两张彩纸:一张红色,一张蓝色。
将其中一张纸放在另一张上。现在将两张纸一起揉成小球。这个皱巴巴的纸球就是你的输入数
据,每张纸对应于分类问题中的一个类别。神经网络(或者任何机器学习模型)要做的就是找
到可以让纸球恢复平整的变换,从而能够再次让两个类别明确可分。通过深度学习,这一过程
可以用三维空间中一系列简单的变换来实现,比如你用手指对纸球做的变换,每次做一个动作,
如图2-9 所示。