2.3. 线性代数
Open the notebook in Colab
Open the notebook in Colab
Open the notebook in Colab

在你已经可以存储和操作数据后,让我们简要地回顾一下基本线性代数的部分内容。这些内容能够帮助你了解和实现本书中介绍的大多数模型。下面我们将介绍线性代数中的基本数学对象、算术和运算,并用数学符号和相应的代码实现来表示它们。

2.3.1. 标量

如果你从来没有学过线性代数或机器学习,那么你过去的数学经历可能是一次只想一个数字。如果你曾经报销过发票,或者在餐厅支付餐费,那么你已经知道如何做一些基本的事情,比如在数字间相加或相乘。例如,北京的温度为\(52\)华氏度(除了摄氏度外,另一种温度刻度)。严格来说,我们称仅包含一个数值的叫标量(scalar)。如果要将此华氏度值转换为更常用的摄氏度,则可以计算表达式\(c=\frac{5}{9}(f-32)\),并将\(f\)赋为\(52\)。在此等式中,每一项(\(5\)\(9\)\(32\))都是标量值。符号\(c\)\(f\)称为变量(variables),它们表示未知的标量值。

在本书中,我们采用了数学表示法,其中标量变量由普通小写字母表示(例如,\(x\)\(y\)\(z\))。我们用\(\mathbb{R}\)表示所有(连续)实数标量的空间。为了方便,我们之后将严格定义空间(space)是什么,但现在只要记住,表达式\(x\in\mathbb{R}\)是表示\(x\)是一个实值标量的正式形式。符号\(\in\)称为“属于”,它表示“是集合中的成员”。我们可以用\(x, y \in \{0,1\}\)来表明\(x\)\(y\)是值只能为\(0\)\(1\)的数字。

标量由只有一个元素的张量表示。在下面的代码中,我们实例化两个标量,并使用它们执行一些熟悉的算术运算,即加法,乘法,除法和指数。

from mxnet import np, npx

npx.set_np()

x = np.array(3.0)
y = np.array(2.0)

x + y, x * y, x / y, x**y
(array(5.), array(6.), array(1.5), array(9.))
import torch

x = torch.tensor([3.0])
y = torch.tensor([2.0])

x + y, x * y, x / y, x**y
(tensor([5.]), tensor([6.]), tensor([1.5000]), tensor([9.]))
import tensorflow as tf

x = tf.constant([3.0])
y = tf.constant([2.0])

x + y, x * y, x / y, x**y
(<tf.Tensor: shape=(1,), dtype=float32, numpy=array([5.], dtype=float32)>,
 <tf.Tensor: shape=(1,), dtype=float32, numpy=array([6.], dtype=float32)>,
 <tf.Tensor: shape=(1,), dtype=float32, numpy=array([1.5], dtype=float32)>,
 <tf.Tensor: shape=(1,), dtype=float32, numpy=array([9.], dtype=float32)>)

2.3.2. 向量

你可以将向量视为标量值组成的列表。我们将这些标量值称为向量的元素(elements)或分量(components)。当我们的向量表示数据集中的样本时,它们的值具有一定的现实意义。例如,如果我们正在训练一个模型来预测贷款违约风险,我们可能会将每个申请人与一个向量相关联,其分量与其收入、工作年限、过往违约次数和其他因素相对应。如果我们正在研究医院患者可能面临的心脏病发作风险,我们可能会用一个向量来表示每个患者,其分量为最近的生命体征、胆固醇水平、每天运动时间等。在数学表示法中,我们通常将向量记为粗体、小写的符号(例如,\(\mathbf{x}\)\(\mathbf{y}\)\(\mathbf{z})\))。

我们通过一维张量处理向量。一般来说,张量可以具有任意长度,取决于机器的内存限制。

x = np.arange(4)
x
array([0., 1., 2., 3.])
x = torch.arange(4)
x
tensor([0, 1, 2, 3])
x = tf.range(4)
x
<tf.Tensor: shape=(4,), dtype=int32, numpy=array([0, 1, 2, 3], dtype=int32)>

我们可以使用下标来引用向量的任一元素。例如,我们可以通过\(x_i\)来引用第\(i\)个元素。注意,元素\(x_i\)是一个标量,所以我们在引用它时不会加粗。大量文献认为列向量是向量的默认方向,在本书中也是如此。在数学中,向量\(\mathbf{x}\)可以写为:

(2.3.1)\[\begin{split}\mathbf{x} =\begin{bmatrix}x_{1} \\x_{2} \\ \vdots \\x_{n}\end{bmatrix},\end{split}\]

其中\(x_1,\ldots,x_n\)是向量的元素。在代码中,我们通过张量的索引来访问任一元素。

x[3]
array(3.)
x[3]
tensor(3)
x[3]
<tf.Tensor: shape=(), dtype=int32, numpy=3>

2.3.2.1. 长度、维度和形状

让我们回顾一下 2.1节中的一些概念。向量只是一个数字数组。就像每个数组都有一个长度一样,每个向量也是如此。在数学表示法中,如果我们想说一个向量\(\mathbf{x}\)\(n\)个实值标量组成,我们可以将其表示为\(\mathbf{x}\in\mathbb{R}^n\)。向量的长度通常称为向量的维度(dimension)。

与普通的Python数组一样,我们可以通过调用Python的内置len()函数来访问张量的长度。

len(x)
4
len(x)
4
len(x)
4

当用张量表示一个向量(只有一个轴)时,我们也可以通过.shape属性访问向量的长度。形状(shape)是一个元组,列出了张量沿每个轴的长度(维数)。对于只有一个轴的张量,形状只有一个元素。

x.shape
(4,)
x.shape
torch.Size([4])
x.shape
TensorShape([4])

请注意,维度(dimension)这个词在不同上下文时往往会有不同的含义,这经常会使人感到困惑。为了清楚起见,我们在此明确一下。向量的维度被用来表示向量的长度,即向量或轴的元素数量。然而,张量的维度用来表示张量具有的轴数。在这个意义上,张量的某个轴的维数就是这个轴的长度。

2.3.3. 矩阵

正如向量将标量从零阶推广到一阶,矩阵将向量从一阶推广到二阶。矩阵,我们通常用粗体、大写字母来表示(例如,\(\mathbf{X}\)\(\mathbf{Y}\)\(\mathbf{Z}\)),在代码中表示为具有两个轴的张量。

在数学表示法中,我们使用\(\mathbf{A} \in \mathbb{R}^{m \times n}\)来表示矩阵\(\mathbf{A}\),其由\(m\)行和\(n\)列的实值标量组成。直观地,我们可以将任意矩阵\(\mathbf{A} \in \mathbb{R}^{m \times n}\)视为一个表格,其中每个元素\(a_{ij}\)属于第\(i\)行第\(j\)列:

(2.3.2)\[\begin{split}\mathbf{A}=\begin{bmatrix} a_{11} & a_{12} & \cdots & a_{1n} \\ a_{21} & a_{22} & \cdots & a_{2n} \\ \vdots & \vdots & \ddots & \vdots \\ a_{m1} & a_{m2} & \cdots & a_{mn} \\ \end{bmatrix}.\end{split}\]

对于任意\(\mathbf{A} \in \mathbb{R}^{m \times n}\),\(\mathbf{A}\)的形状是(\(m\), \(n\))或\(m \times n\)。当矩阵具有相同数量的行和列时,其形状将变为正方形;因此,它被称为方矩阵(square matrix)。

当调用函数来实例化张量时,我们可以通过指定两个分量\(m\)\(n\)来创建一个形状为\(m \times n\)的矩阵。

A = np.arange(20).reshape(5, 4)
A
array([[ 0.,  1.,  2.,  3.],
       [ 4.,  5.,  6.,  7.],
       [ 8.,  9., 10., 11.],
       [12., 13., 14., 15.],
       [16., 17., 18., 19.]])
A = torch.arange(20).reshape(5, 4)
A
tensor([[ 0,  1,  2,  3],
        [ 4,  5,  6,  7],
        [ 8,  9, 10, 11],
        [12, 13, 14, 15],
        [16, 17, 18, 19]])
A = tf.reshape(tf.range(20), (5, 4))
A
<tf.Tensor: shape=(5, 4), dtype=int32, numpy=
array([[ 0,  1,  2,  3],
       [ 4,  5,  6,  7],
       [ 8,  9, 10, 11],
       [12, 13, 14, 15],
       [16, 17, 18, 19]], dtype=int32)>

我们可以通过行索引(\(i\))和列索引(\(j\))来访问矩阵中的标量元素\(a_{ij}\),例如\([\mathbf{A}]_{ij}\)。如果没有给出矩阵\(\mathbf{A}\)的标量元素,如在 (2.3.2)那样,我们可以简单地使用矩阵\(\mathbf{A}\)的小写字母索引下标\(a_{ij}\)来引用\([\mathbf{A}]_{ij}\)。为了表示起来简单,只有在必要时才会将逗号插入到单独的索引中,例如\(a_{2,3j}\)\([\mathbf{A}]_{2i-1,3}\)

有时候,我们想翻转轴。当我们交换矩阵的行和列时,结果称为矩阵的转置(transpose)。我们用\(\mathbf{a}^\top\)来表示矩阵的转置,如果\(\mathbf{B}=\mathbf{A}^\top\),则对于任意\(i\)\(j\),都有\(b_{ij}=a_{ji}\)。因此,在 (2.3.2)中的转置是一个形状为\(n \times m\)的矩阵:

(2.3.3)\[\begin{split}\mathbf{A}^\top = \begin{bmatrix} a_{11} & a_{21} & \dots & a_{m1} \\ a_{12} & a_{22} & \dots & a_{m2} \\ \vdots & \vdots & \ddots & \vdots \\ a_{1n} & a_{2n} & \dots & a_{mn} \end{bmatrix}.\end{split}\]

现在我们在代码中访问矩阵的转置。

A.T
array([[ 0.,  4.,  8., 12., 16.],
       [ 1.,  5.,  9., 13., 17.],
       [ 2.,  6., 10., 14., 18.],
       [ 3.,  7., 11., 15., 19.]])
A.T
tensor([[ 0,  4,  8, 12, 16],
        [ 1,  5,  9, 13, 17],
        [ 2,  6, 10, 14, 18],
        [ 3,  7, 11, 15, 19]])
tf.transpose(A)
<tf.Tensor: shape=(4, 5), dtype=int32, numpy=
array([[ 0,  4,  8, 12, 16],
       [ 1,  5,  9, 13, 17],
       [ 2,  6, 10, 14, 18],
       [ 3,  7, 11, 15, 19]], dtype=int32)>

作为方矩阵的一种特殊类型,对称矩阵(symmetric matrix)\(\mathbf{A}\)等于其转置:\(\mathbf{A} = \mathbf{A}^\top\)。这里我们定义一个对称矩阵 B

B = np.array([[1, 2, 3], [2, 0, 4], [3, 4, 5]])
B
array([[1., 2., 3.],
       [2., 0., 4.],
       [3., 4., 5.]])
B = torch.tensor([[1, 2, 3], [2, 0, 4], [3, 4, 5]])
B
tensor([[1, 2, 3],
        [2, 0, 4],
        [3, 4, 5]])
B = tf.constant([[1, 2, 3], [2, 0, 4], [3, 4, 5]])
B
<tf.Tensor: shape=(3, 3), dtype=int32, numpy=
array([[1, 2, 3],
       [2, 0, 4],
       [3, 4, 5]], dtype=int32)>

现在我们将B与它的转置进行比较。

B == B.T
array([[ True,  True,  True],
       [ True,  True,  True],
       [ True,  True,  True]])
B == B.T
tensor([[True, True, True],
        [True, True, True],
        [True, True, True]])
B == tf.transpose(B)
<tf.Tensor: shape=(3, 3), dtype=bool, numpy=
array([[ True,  True,  True],
       [ True,  True,  True],
       [ True,  True,  True]])>

矩阵是有用的数据结构:它们允许我们组织具有不同变化模式的数据。例如,我们矩阵中的行可能对应于不同的房屋(数据样本),而列可能对应于不同的属性。如果你曾经使用过电子表格软件或已阅读过 2.2节,这应该听起来很熟悉。因此,尽管单个向量的默认方向是列向量,但在表示表格数据集的矩阵中,将每个数据样本作为矩阵中的行向量更为常见。我们将在后面的章节中讲到这点。这种约定将支持常见的深度学习实践。例如,沿着张量的最外轴,我们可以访问或遍历小批量的数据样本。如果不存在小批量,我们也可以只访问数据样本。

2.3.4. 张量

就像向量是标量的推广,矩阵是向量的推广一样,我们可以构建具有更多轴的数据结构。张量(本小节中的“张量”指代数对象)为我们提供了描述具有任意数量轴的\(n\)维数组的通用方法。例如,向量是一阶张量,矩阵是二阶张量。张量用特殊字体的大写字母(例如,\(\mathsf{X}\)\(\mathsf{Y}\)\(\mathsf{Z}\))表示,它们的索引机制(例如\(x_{ijk}\)\([\mathsf{X}]_{1,2i-1,3}\))与矩阵类似。

当我们开始处理图像时,张量将变得更加重要,图像以\(n\)维数组形式出现,其中3个轴对应于高度、宽度,以及一个通道(channel)轴,用于堆叠颜色通道(红色、绿色和蓝色)。现在,我们将跳过高阶张量,集中在基础知识上。

X = np.arange(24).reshape(2, 3, 4)
X
array([[[ 0.,  1.,  2.,  3.],
        [ 4.,  5.,  6.,  7.],
        [ 8.,  9., 10., 11.]],

       [[12., 13., 14., 15.],
        [16., 17., 18., 19.],
        [20., 21., 22., 23.]]])
X = torch.arange(24).reshape(2, 3, 4)
X
tensor([[[ 0,  1,  2,  3],
         [ 4,  5,  6,  7],
         [ 8,  9, 10, 11]],

        [[12, 13, 14, 15],
         [16, 17, 18, 19],
         [20, 21, 22, 23]]])
X = tf.reshape(tf.range(24), (2, 3, 4))
X
<tf.Tensor: shape=(2, 3, 4), dtype=int32, numpy=
array([[[ 0,  1,  2,  3],
        [ 4,  5,  6,  7],
        [ 8,  9, 10, 11]],

       [[12, 13, 14, 15],
        [16, 17, 18, 19],
        [20, 21, 22, 23]]], dtype=int32)>

2.3.5. 张量算法的基本性质

标量、向量、矩阵和任意数量轴的张量(本小节中的“张量”指代数对象)有一些很好的属性,通常会派上用场。例如,你可能已经从按元素操作的定义中注意到,任何按元素的一元运算都不会改变其操作数的形状。同样,给定具有相同形状的任意两个张量,任何按元素二元运算的结果都将是相同形状的张量。例如,将两个相同形状的矩阵相加会在这两个矩阵上执行元素加法。

A = np.arange(20).reshape(5, 4)
B = A.copy()  # 通过分配新内存,将A的一个副本分配给B
A, A + B
(array([[ 0.,  1.,  2.,  3.],
        [ 4.,  5.,  6.,  7.],
        [ 8.,  9., 10., 11.],
        [12., 13., 14., 15.],
        [16., 17., 18., 19.]]),
 array([[ 0.,  2.,  4.,  6.],
        [ 8., 10., 12., 14.],
        [16., 18., 20., 22.],
        [24., 26., 28., 30.],
        [32., 34., 36., 38.]]))
A = torch.arange(20, dtype=torch.float32).reshape(5, 4)
B = A.clone()  # 通过分配新内存,将A的一个副本分配给B
A, A + B
(tensor([[ 0.,  1.,  2.,  3.],
         [ 4.,  5.,  6.,  7.],
         [ 8.,  9., 10., 11.],
         [12., 13., 14., 15.],
         [16., 17., 18., 19.]]),
 tensor([[ 0.,  2.,  4.,  6.],
         [ 8., 10., 12., 14.],
         [16., 18., 20., 22.],
         [24., 26., 28., 30.],
         [32., 34., 36., 38.]]))
A = tf.reshape(tf.range(20, dtype=tf.float32), (5, 4))
B = A  # 不能通过分配新内存将A克隆到B
A, A + B
(<tf.Tensor: shape=(5, 4), dtype=float32, numpy=
 array([[ 0.,  1.,  2.,  3.],
        [ 4.,  5.,  6.,  7.],
        [ 8.,  9., 10., 11.],
        [12., 13., 14., 15.],
        [16., 17., 18., 19.]], dtype=float32)>,
 <tf.Tensor: shape=(5, 4), dtype=float32, numpy=
 array([[ 0.,  2.,  4.,  6.],
        [ 8., 10., 12., 14.],
        [16., 18., 20., 22.],
        [24., 26., 28., 30.],
        [32., 34., 36., 38.]], dtype=float32)>)

具体而言,两个矩阵的按元素乘法称为哈达玛积(Hadamard product)(数学符号\(\odot\))。对于矩阵\(\mathbf{B} \in \mathbb{R}^{m \times n}\),其中第\(i\)行和第\(j\)列的元素是\(b_{ij}\)。矩阵\(\mathbf{A}\)(在 (2.3.2)中定义)和\(\mathbf{B}\)的哈达玛积为:

(2.3.4)\[\begin{split}\mathbf{A} \odot \mathbf{B} = \begin{bmatrix} a_{11} b_{11} & a_{12} b_{12} & \dots & a_{1n} b_{1n} \\ a_{21} b_{21} & a_{22} b_{22} & \dots & a_{2n} b_{2n} \\ \vdots & \vdots & \ddots & \vdots \\ a_{m1} b_{m1} & a_{m2} b_{m2} & \dots & a_{mn} b_{mn} \end{bmatrix}.\end{split}\]
A * B
array([[  0.,   1.,   4.,   9.],
       [ 16.,  25.,  36.,  49.],
       [ 64.,  81., 100., 121.],
       [144., 169., 196., 225.],
       [256., 289., 324., 361.]])
A * B
tensor([[  0.,   1.,   4.,   9.],
        [ 16.,  25.,  36.,  49.],
        [ 64.,  81., 100., 121.],
        [144., 169., 196., 225.],
        [256., 289., 324., 361.]])
A * B
<tf.Tensor: shape=(5, 4), dtype=float32, numpy=
array([[  0.,   1.,   4.,   9.],
       [ 16.,  25.,  36.,  49.],
       [ 64.,  81., 100., 121.],
       [144., 169., 196., 225.],
       [256., 289., 324., 361.]], dtype=float32)>

将张量乘以或加上一个标量不会改变张量的形状,其中张量的每个元素都将与标量相加或相乘。

a = 2
X = np.arange(24).reshape(2, 3, 4)
a + X, (a * X).shape
(array([[[ 2.,  3.,  4.,  5.],
         [ 6.,  7.,  8.,  9.],
         [10., 11., 12., 13.]],

        [[14., 15., 16., 17.],
         [18., 19., 20., 21.],
         [22., 23., 24., 25.]]]),
 (2, 3, 4))
a = 2
X = torch.arange(24).reshape(2, 3, 4)
a + X, (a * X).shape
(tensor([[[ 2,  3,  4,  5],
          [ 6,  7,  8,  9],
          [10, 11, 12, 13]],

         [[14, 15, 16, 17],
          [18, 19, 20, 21],
          [22, 23, 24, 25]]]),
 torch.Size([2, 3, 4]))
a = 2
X = tf.reshape(tf.range(24), (2, 3, 4))
a + X, (a * X).shape
(<tf.Tensor: shape=(2, 3, 4), dtype=int32, numpy=
 array([[[ 2,  3,  4,  5],
         [ 6,  7,  8,  9],
         [10, 11, 12, 13]],

        [[14, 15, 16, 17],
         [18, 19, 20, 21],
         [22, 23, 24, 25]]], dtype=int32)>,
 TensorShape([2, 3, 4]))

2.3.6. 降维

我们可以对任意张量进行的一个有用的操作是计算其元素的和。在数学表示法中,我们使用\(\sum\)符号表示求和。为了表示长度为\(d\)的向量中元素的总和,可以记为\(\sum_{i=1}^dx_i\)。在代码中,我们可以调用计算求和的函数:

x = np.arange(4)
x, x.sum()
(array([0., 1., 2., 3.]), array(6.))
x = torch.arange(4, dtype=torch.float32)
x, x.sum()
(tensor([0., 1., 2., 3.]), tensor(6.))
x = tf.range(4, dtype=tf.float32)
x, tf.reduce_sum(x)
(<tf.Tensor: shape=(4,), dtype=float32, numpy=array([0., 1., 2., 3.], dtype=float32)>,
 <tf.Tensor: shape=(), dtype=float32, numpy=6.0>)

我们可以表示任意形状张量的元素和。 例如,矩阵\(\mathbf{A}\)中元素的和可以记为\(\sum_{i=1}^{m} \sum_{j=1}^{n} a_{ij}\)

A.shape, A.sum()
((5, 4), array(190.))
A.shape, A.sum()
(torch.Size([5, 4]), tensor(190.))
A.shape, tf.reduce_sum(A)
(TensorShape([5, 4]), <tf.Tensor: shape=(), dtype=float32, numpy=190.0>)

默认情况下,调用求和函数会沿所有的轴降低张量的维度,使它变为一个标量。 我们还可以指定张量沿哪一个轴来通过求和降低维度。以矩阵为例,为了通过求和所有行的元素来降维(轴0),我们可以在调用函数时指定axis=0。 由于输入矩阵沿0轴降维以生成输出向量,因此输入的轴0的维数在输出形状中丢失。

A_sum_axis0 = A.sum(axis=0)
A_sum_axis0, A_sum_axis0.shape
(array([40., 45., 50., 55.]), (4,))
A_sum_axis0 = A.sum(axis=0)
A_sum_axis0, A_sum_axis0.shape
(tensor([40., 45., 50., 55.]), torch.Size([4]))
A_sum_axis0 = tf.reduce_sum(A, axis=0)
A_sum_axis0, A_sum_axis0.shape
(<tf.Tensor: shape=(4,), dtype=float32, numpy=array([40., 45., 50., 55.], dtype=float32)>,
 TensorShape([4]))

指定axis=1将通过汇总所有列的元素降维(轴1)。因此,输入的轴1的维数在输出形状中消失。

A_sum_axis1 = A.sum(axis=1)
A_sum_axis1, A_sum_axis1.shape
(array([ 6., 22., 38., 54., 70.]), (5,))
A_sum_axis1 = A.sum(axis=1)
A_sum_axis1, A_sum_axis1.shape
(tensor([ 6., 22., 38., 54., 70.]), torch.Size([5]))
A_sum_axis1 = tf.reduce_sum(A, axis=1)
A_sum_axis1, A_sum_axis1.shape
(<tf.Tensor: shape=(5,), dtype=float32, numpy=array([ 6., 22., 38., 54., 70.], dtype=float32)>,
 TensorShape([5]))

沿着行和列对矩阵求和,等价于对矩阵的所有元素进行求和。

A.sum(axis=[0, 1])  # Same as `A.sum()`
array(190.)
A.sum(axis=[0, 1])  # Same as `A.sum()`
tensor(190.)
tf.reduce_sum(A, axis=[0, 1])  # Same as `tf.reduce_sum(A)`
<tf.Tensor: shape=(), dtype=float32, numpy=190.0>

一个与求和相关的量是平均值(mean或average)。我们通过将总和除以元素总数来计算平均值。在代码中,我们可以调用函数来计算任意形状张量的平均值。

A.mean(), A.sum() / A.size
(array(9.5), array(9.5))
A.mean(), A.sum() / A.numel()
(tensor(9.5000), tensor(9.5000))
tf.reduce_mean(A), tf.reduce_sum(A) / tf.size(A).numpy()
(<tf.Tensor: shape=(), dtype=float32, numpy=9.5>,
 <tf.Tensor: shape=(), dtype=float32, numpy=9.5>)

同样,计算平均值的函数也可以沿指定轴降低张量的维度。

A.mean(axis=0), A.sum(axis=0) / A.shape[0]
(array([ 8.,  9., 10., 11.]), array([ 8.,  9., 10., 11.]))
A.mean(axis=0), A.sum(axis=0) / A.shape[0]
(tensor([ 8.,  9., 10., 11.]), tensor([ 8.,  9., 10., 11.]))
tf.reduce_mean(A, axis=0), tf.reduce_sum(A, axis=0) / A.shape[0]
(<tf.Tensor: shape=(4,), dtype=float32, numpy=array([ 8.,  9., 10., 11.], dtype=float32)>,
 <tf.Tensor: shape=(4,), dtype=float32, numpy=array([ 8.,  9., 10., 11.], dtype=float32)>)

2.3.6.1. 非降维求和

但是,有时在调用函数来计算总和或均值时保持轴数不变会很有用。

sum_A = A.sum(axis=1, keepdims=True)
sum_A
array([[ 6.],
       [22.],
       [38.],
       [54.],
       [70.]])
sum_A = A.sum(axis=1, keepdims=True)
sum_A
tensor([[ 6.],
        [22.],
        [38.],
        [54.],
        [70.]])
sum_A = tf.reduce_sum(A, axis=1, keepdims=True)
sum_A
<tf.Tensor: shape=(5, 1), dtype=float32, numpy=
array([[ 6.],
       [22.],
       [38.],
       [54.],
       [70.]], dtype=float32)>

例如,由于sum_A在对每行进行求和后仍保持两个轴,我们可以通过广播将A除以sum_A

A / sum_A
array([[0.        , 0.16666667, 0.33333334, 0.5       ],
       [0.18181819, 0.22727273, 0.27272728, 0.3181818 ],
       [0.21052632, 0.23684211, 0.2631579 , 0.28947368],
       [0.22222222, 0.24074075, 0.25925925, 0.2777778 ],
       [0.22857143, 0.24285714, 0.25714287, 0.27142859]])
A / sum_A
tensor([[0.0000, 0.1667, 0.3333, 0.5000],
        [0.1818, 0.2273, 0.2727, 0.3182],
        [0.2105, 0.2368, 0.2632, 0.2895],
        [0.2222, 0.2407, 0.2593, 0.2778],
        [0.2286, 0.2429, 0.2571, 0.2714]])
A / sum_A
<tf.Tensor: shape=(5, 4), dtype=float32, numpy=
array([[0.        , 0.16666667, 0.33333334, 0.5       ],
       [0.18181819, 0.22727273, 0.27272728, 0.3181818 ],
       [0.21052632, 0.23684211, 0.2631579 , 0.28947368],
       [0.22222222, 0.24074075, 0.25925925, 0.2777778 ],
       [0.22857143, 0.24285714, 0.25714287, 0.27142859]], dtype=float32)>

如果我们想沿某个轴计算A元素的累积总和,比如axis=0(按行计算),我们可以调用cumsum函数。此函数不会沿任何轴降低输入张量的维度。

A.cumsum(axis=0)
array([[ 0.,  1.,  2.,  3.],
       [ 4.,  6.,  8., 10.],
       [12., 15., 18., 21.],
       [24., 28., 32., 36.],
       [40., 45., 50., 55.]])
A.cumsum(axis=0)
tensor([[ 0.,  1.,  2.,  3.],
        [ 4.,  6.,  8., 10.],
        [12., 15., 18., 21.],
        [24., 28., 32., 36.],
        [40., 45., 50., 55.]])
tf.cumsum(A, axis=0)
<tf.Tensor: shape=(5, 4), dtype=float32, numpy=
array([[ 0.,  1.,  2.,  3.],
       [ 4.,  6.,  8., 10.],
       [12., 15., 18., 21.],
       [24., 28., 32., 36.],
       [40., 45., 50., 55.]], dtype=float32)>

2.3.7. 点积(Dot Product)

到目前为止,我们只执行了按元素操作、求和及平均值。如果这就是我们所能做的,那么线性代数可能就不需要单独一节了。 但是,最基本的操作之一是点积。给定两个向量\(\mathbf{x},\mathbf{y}\in\mathbb{R}^d\),它们的点积(dotproduct)\(\mathbf{x}^\top\mathbf{y}\)(或\(\langle\mathbf{x},\mathbf{y}\rangle\))是相同位置的按元素乘积的和:\(\mathbf{x}^\top \mathbf{y} = \sum_{i=1}^{d} x_i y_i\)

y = np.ones(4)
x, y, np.dot(x, y)
(array([0., 1., 2., 3.]), array([1., 1., 1., 1.]), array(6.))
y = torch.ones(4, dtype=torch.float32)
x, y, torch.dot(x, y)
(tensor([0., 1., 2., 3.]), tensor([1., 1., 1., 1.]), tensor(6.))
y = tf.ones(4, dtype=tf.float32)
x, y, tf.tensordot(x, y, axes=1)
(<tf.Tensor: shape=(4,), dtype=float32, numpy=array([0., 1., 2., 3.], dtype=float32)>,
 <tf.Tensor: shape=(4,), dtype=float32, numpy=array([1., 1., 1., 1.], dtype=float32)>,
 <tf.Tensor: shape=(), dtype=float32, numpy=6.0>)

注意,我们可以通过执行按元素乘法,然后进行求和来表示两个向量的点积:

np.sum(x * y)
array(6.)
torch.sum(x * y)
tensor(6.)
tf.reduce_sum(x * y)
<tf.Tensor: shape=(), dtype=float32, numpy=6.0>

点积在很多场合都很有用。例如,给定一组由向量\(\mathbf{x} \in \mathbb{R}^d\)表示的值,和一组由\(\mathbf{w} \in \mathbb{R}^d\)表示的权重。\(\mathbf{x}\)中的值根据权重\(\mathbf{w}\)的加权和可以表示为点积\(\mathbf{x}^\top \mathbf{w}\)。当权重为非负数且和为1(即\(\left(\sum_{i=1}^{d}{w_i}=1\right)\))时,点积表示加权平均(weighted average)。将两个向量归一化得到单位长度后,点积表示它们夹角的余弦。我们将在本节的后面正式介绍长度(length)的概念。

2.3.8. 矩阵-向量积

现在我们知道如何计算点积,我们可以开始理解矩阵-向量积(matrix-vector products)。回顾分别在 (2.3.2)(2.3.1)中定义并画出的矩阵\(\mathbf{A} \in \mathbb{R}^{m \times n}\)和向量\(\mathbf{x} \in \mathbb{R}^n\)。让我们将矩阵\(\mathbf{A}\)用它的行向量表示

(2.3.5)\[\begin{split}\mathbf{A}= \begin{bmatrix} \mathbf{a}^\top_{1} \\ \mathbf{a}^\top_{2} \\ \vdots \\ \mathbf{a}^\top_m \\ \end{bmatrix},\end{split}\]

其中每个\(\mathbf{a}^\top_{i} \in \mathbb{R}^n\) 都是行向量,表示矩阵的第\(i\)行。矩阵向量积\(\mathbf{A}\mathbf{x}\)是一个长度为\(m\)的列向量,其第\(i\)个元素是点积\(\mathbf{a}^\top_i \mathbf{x}\)

(2.3.6)\[\begin{split}\mathbf{A}\mathbf{x} = \begin{bmatrix} \mathbf{a}^\top_{1} \\ \mathbf{a}^\top_{2} \\ \vdots \\ \mathbf{a}^\top_m \\ \end{bmatrix}\mathbf{x} = \begin{bmatrix} \mathbf{a}^\top_{1} \mathbf{x} \\ \mathbf{a}^\top_{2} \mathbf{x} \\ \vdots\\ \mathbf{a}^\top_{m} \mathbf{x}\\ \end{bmatrix}.\end{split}\]

我们可以把一个矩阵\(\mathbf{A} \in \mathbb{R}^{m \times n}\)乘法看作是一个从\(\mathbb{R}^{n}\)\(\mathbb{R}^{m}\)向量的转换。这些转换证明是非常有用的。例如,我们可以用方阵的乘法来表示旋转。 我们将在后续章节中讲到,我们也可以使用矩阵-向量积来描述在给定前一层的值时,求解神经网络每一层所需的复杂计算。

在代码中使用张量表示矩阵-向量积,我们使用与点积相同的dot函数。当我们为矩阵A和向量x调用np.dot(A,x)时,会执行矩阵-向量积。注意,A的列维数(沿轴1的长度)必须与x的维数(其长度)相同。

A.shape, x.shape, np.dot(A, x)
((5, 4), (4,), array([ 14.,  38.,  62.,  86., 110.]))
A.shape, x.shape, torch.mv(A, x)
(torch.Size([5, 4]), torch.Size([4]), tensor([ 14.,  38.,  62.,  86., 110.]))
A.shape, x.shape, tf.linalg.matvec(A, x)
(TensorShape([5, 4]),
 TensorShape([4]),
 <tf.Tensor: shape=(5,), dtype=float32, numpy=array([ 14.,  38.,  62.,  86., 110.], dtype=float32)>)

2.3.9. 矩阵-矩阵乘法

如果你已经掌握了点积和矩阵-向量积的知识,那么矩阵-矩阵乘法(matrix-matrix multiplication)应该很简单。

假设我们有两个矩阵\(\mathbf{A} \in \mathbb{R}^{n \times k}\)\(\mathbf{B} \in \mathbb{R}^{k \times m}\)

(2.3.7)\[\begin{split}\mathbf{A}=\begin{bmatrix} a_{11} & a_{12} & \cdots & a_{1k} \\ a_{21} & a_{22} & \cdots & a_{2k} \\ \vdots & \vdots & \ddots & \vdots \\ a_{n1} & a_{n2} & \cdots & a_{nk} \\ \end{bmatrix},\quad \mathbf{B}=\begin{bmatrix} b_{11} & b_{12} & \cdots & b_{1m} \\ b_{21} & b_{22} & \cdots & b_{2m} \\ \vdots & \vdots & \ddots & \vdots \\ b_{k1} & b_{k2} & \cdots & b_{km} \\ \end{bmatrix}.\end{split}\]

用行向量\(\mathbf{a}^\top_{i} \in \mathbb{R}^k\)表示矩阵\(\mathbf{A}\)的第 \(i\)行,并让列向量\(\mathbf{b}_{j} \in \mathbb{R}^k\)作为矩阵\(\mathbf{B}\)的第 \(j\) 列。要生成矩阵积\(\mathbf{C} = \mathbf{A}\mathbf{B}\),最简单的方法是考虑\(\mathbf{A}\)的行向量和\(\mathbf{B}\)的列向量:

(2.3.8)\[\begin{split}\mathbf{A}= \begin{bmatrix} \mathbf{a}^\top_{1} \\ \mathbf{a}^\top_{2} \\ \vdots \\ \mathbf{a}^\top_n \\ \end{bmatrix}, \quad \mathbf{B}=\begin{bmatrix} \mathbf{b}_{1} & \mathbf{b}_{2} & \cdots & \mathbf{b}_{m} \\ \end{bmatrix}.\end{split}\]

当我们简单地将每个元素\(c_{ij}\)计算为点积\(\mathbf{a}^\top_i \mathbf{b}_j\):

(2.3.9)\[\begin{split}\mathbf{C} = \mathbf{AB} = \begin{bmatrix} \mathbf{a}^\top_{1} \\ \mathbf{a}^\top_{2} \\ \vdots \\ \mathbf{a}^\top_n \\ \end{bmatrix} \begin{bmatrix} \mathbf{b}_{1} & \mathbf{b}_{2} & \cdots & \mathbf{b}_{m} \\ \end{bmatrix} = \begin{bmatrix} \mathbf{a}^\top_{1} \mathbf{b}_1 & \mathbf{a}^\top_{1}\mathbf{b}_2& \cdots & \mathbf{a}^\top_{1} \mathbf{b}_m \\ \mathbf{a}^\top_{2}\mathbf{b}_1 & \mathbf{a}^\top_{2} \mathbf{b}_2 & \cdots & \mathbf{a}^\top_{2} \mathbf{b}_m \\ \vdots & \vdots & \ddots &\vdots\\ \mathbf{a}^\top_{n} \mathbf{b}_1 & \mathbf{a}^\top_{n}\mathbf{b}_2& \cdots& \mathbf{a}^\top_{n} \mathbf{b}_m \end{bmatrix}.\end{split}\]

我们可以将矩阵-矩阵乘法\(\mathbf{AB}\)看作是简单地执行\(m\)次矩阵-向量积,并将结果拼接在一起,形成一个\(n \times m\)矩阵。在下面的代码中,我们在AB上执行矩阵乘法。这里的A是一个5行4列的矩阵,B是一个4行3列的矩阵。相乘后,我们得到了一个5行3列的矩阵。

B = np.ones(shape=(4, 3))
np.dot(A, B)
array([[ 6.,  6.,  6.],
       [22., 22., 22.],
       [38., 38., 38.],
       [54., 54., 54.],
       [70., 70., 70.]])
B = torch.ones(4, 3)
torch.mm(A, B)
tensor([[ 6.,  6.,  6.],
        [22., 22., 22.],
        [38., 38., 38.],
        [54., 54., 54.],
        [70., 70., 70.]])
B = tf.ones((4, 3), tf.float32)
tf.matmul(A, B)
<tf.Tensor: shape=(5, 3), dtype=float32, numpy=
array([[ 6.,  6.,  6.],
       [22., 22., 22.],
       [38., 38., 38.],
       [54., 54., 54.],
       [70., 70., 70.]], dtype=float32)>

矩阵-矩阵乘法可以简单地称为矩阵乘法,不应与“哈达玛积”混淆。

2.3.10. 范数

线性代数中最有用的一些运算符是范数(norms)。非正式地说,一个向量的范数告诉我们一个向量有多大。 这里考虑的大小(size)概念不涉及维度,而是分量的大小。

在线性代数中,向量范数是将向量映射到标量的函数\(f\)。向量范数要满足一些属性。 给定任意向量\(\mathbf{x}\),第一个性质说,如果我们按常数因子\(\alpha\)缩放向量的所有元素,其范数也会按相同常数因子的绝对值缩放:

(2.3.10)\[f(\alpha \mathbf{x}) = |\alpha| f(\mathbf{x}).\]

第二个性质是我们熟悉的三角不等式:

(2.3.11)\[f(\mathbf{x} + \mathbf{y}) \leq f(\mathbf{x}) + f(\mathbf{y}).\]

第三个性质简单地说范数必须是非负的:

(2.3.12)\[f(\mathbf{x}) \geq 0.\]

这是有道理的,因为在大多数情况下,任何东西的最小的大小是0。最后一个性质要求范数最小为0,当且仅当向量全由0组成。

(2.3.13)\[\forall i, [\mathbf{x}]_i = 0 \Leftrightarrow f(\mathbf{x})=0.\]

你可能会注意到,范数听起来很像距离的度量。如果你还记得小学时的欧几里得距离(想想毕达哥拉斯定理),那么非负性的概念和三角不等式可能会给你一些启发。 事实上,欧几里得距离是一个范数:具体而言,它是\(L_2\)范数。假设\(n\)维向量\(\mathbf{x}\)中的元素是\(x_1,\ldots,x_n\),其\(L_2\)范数是向量元素平方和的平方根:

(2.3.14)\[\|\mathbf{x}\|_2 = \sqrt{\sum_{i=1}^n x_i^2},\]

其中,在\(L_2\)范数中常常省略下标\(2\),也就是说,\(\|\mathbf{x}\|\)等同于\(\|\mathbf{x}\|_2\)。在代码中,我们可以按如下方式计算向量的\(L_2\)范数。

u = np.array([3, -4])
np.linalg.norm(u)
array(5.)
u = torch.tensor([3.0, -4.0])
torch.norm(u)
tensor(5.)
u = tf.constant([3.0, -4.0])
tf.norm(u)
<tf.Tensor: shape=(), dtype=float32, numpy=5.0>

在深度学习中,我们更经常地使用\(L_2\)范数的平方。你还会经常遇到\(L_1\)范数,它表示为向量元素的绝对值之和:

(2.3.15)\[\|\mathbf{x}\|_1 = \sum_{i=1}^n \left|x_i \right|.\]

\(L_2\)范数相比,\(L_1\)范数受异常值的影响较小。为了计算\(L_1\)范数,我们将绝对值函数和按元素求和组合起来。

np.abs(u).sum()
array(7.)
torch.abs(u).sum()
tensor(7.)
tf.reduce_sum(tf.abs(u))
<tf.Tensor: shape=(), dtype=float32, numpy=7.0>

\(L_2\)范数和\(L_1\)范数都是更一般的\(L_p\)范数的特例:

(2.3.16)\[\|\mathbf{x}\|_p = \left(\sum_{i=1}^n \left|x_i \right|^p \right)^{1/p}.\]

类似于向量的\(L_2\)范数,矩阵\(\mathbf{X} \in \mathbb{R}^{m \times n}\)弗罗贝尼乌斯范数(Frobenius norm)是矩阵元素平方和的平方根:

(2.3.17)\[\|\mathbf{X}\|_F = \sqrt{\sum_{i=1}^m \sum_{j=1}^n x_{ij}^2}.\]

弗罗贝尼乌斯范数满足向量范数的所有性质,它就像是矩阵形向量的\(L_2\)范数。 调用以下函数将计算矩阵的弗罗贝尼乌斯范数。

np.linalg.norm(np.ones((4, 9)))
array(6.)
torch.norm(torch.ones((4, 9)))
tensor(6.)
tf.norm(tf.ones((4, 9)))
<tf.Tensor: shape=(), dtype=float32, numpy=6.0>

2.3.10.1. 范数和目标

虽然我们不想走得太远,但我们可以对这些概念为什么有用有一些直觉。在深度学习中,我们经常试图解决优化问题: 最大化分配给观测数据的概率; 最小化预测和真实观测之间的距离。 用向量表示物品(如单词、产品或新闻文章),以便最小化相似项目之间的距离,最大化不同项目之间的距离。 通常,目标,或许是深度学习算法最重要的组成部分(除了数据),被表达为范数。

2.3.11. 关于线性代数的更多信息

仅用一节,我们就教会了你所需的,用以理解大量的现代深度学习的全部线性代数。 线性代数还有很多,其中很多数学对于机器学习非常有用。例如,矩阵可以分解为因子,这些分解可以显示真实世界数据集中的低维结构。机器学习的整个子领域都侧重于使用矩阵分解及其向高阶张量的泛化来发现数据集中的结构并解决预测问题。但这本书的重点是深度学习。我们相信,一旦你开始动手尝试并在真实数据集上应用了有效的机器学习模型,你会更倾向于学习更多数学。因此,虽然我们保留在后面介绍更多数学知识的权利,但我们这一节到此结束。

如果你渴望了解有关线性代数的更多信息,你可以参考线性代数运算的在线附录或其他优秀资源 [Strang, 1993][Kolter, 2008][Petersen et al., 2008]

2.3.12. 小结

  • 标量、向量、矩阵和张量是线性代数中的基本数学对象。

  • 向量泛化自标量,矩阵泛化自向量。

  • 标量、向量、矩阵和张量分别具有零、一、二和任意数量的轴。

  • 一个张量可以通过summean沿指定的轴降低维度。

  • 两个矩阵的按元素乘法被称为他们的哈达玛积。它与矩阵乘法不同。

  • 在深度学习中,我们经常使用范数,如\(L_1\)范数、\(L_2\)范数和弗罗贝尼乌斯范数。

  • 我们可以对标量、向量、矩阵和张量执行各种操作。

2.3.13. 练习

  1. 证明一个矩阵\(\mathbf{A}\)的转置的转置是 \(\mathbf{A}\)\((\mathbf{A}^\top)^\top = \mathbf{A}\)

  2. 给出两个矩阵\(\mathbf{A}\)\(\mathbf{B}\), 显示转置的和等于和的转置:\(\mathbf{A}^\top + \mathbf{B}^\top = (\mathbf{A} + \mathbf{B})^\top\).

  3. 给定任意方矩阵\(\mathbf{A}\)\(\mathbf{A} + \mathbf{A}^\top\)总是对称的吗?为什么? 1.我们在本节中定义了形状(2,3,4)的张量Xlen(X)的输出结果是什么? 1.对于任意形状的张量X,len(X)是否总是对应于X特定轴的长度?这个轴是什么? 1.运行A/A.sum(axis=1),看看会发生什么。你能分析原因吗? 1.当你在曼哈顿的两点之间旅行时,你需要在坐标上走多远,也就是说,就大街和街道而言?你能斜着走吗? 1.考虑一个具有形状(2,3,4)的张量,在轴0,1,2上的求和输出是什么形状? 1.向linalg.norm函数提供3个或更多轴的张量,并观察其输出。对于任意形状的张量这个函数计算得到什么?