2.3. 线性代数
在 Colab 中打开 Notebook
在 Colab 中打开 Notebook
在 Colab 中打开 Notebook
在 Colab 中打开 Notebook
在 SageMaker Studio Lab 中打开 Notebook

现在,我们已经可以将数据集加载到张量中,并使用基本的数学运算来操作这些张量。为了开始构建复杂的模型,我们还需要一些线性代数工具。本节将温和地介绍最基本的概念,从标量算术开始,逐步升级到矩阵乘法。

import torch
from mxnet import np, npx

npx.set_np()
from jax import numpy as jnp
import tensorflow as tf

2.3.1. 标量

大多数日常数学运算都是一次处理一个数字。正式地说,我们称这些值为标量。例如,帕洛阿尔托的气温是温暖的\(72\)华氏度。如果你想把温度转换为摄氏度,你会计算表达式\(c = \frac{5}{9}(f - 32)\),并将\(f\)设为\(72\)。在这个等式中,值\(5\)\(9\)\(32\)是常数标量。变量\(c\)\(f\)通常表示未知的标量。

我们用普通的小写字母(例如,\(x\)\(y\)\(z\))表示标量,并用\(\mathbb{R}\)表示所有(连续的)实值标量的空间。为了方便起见,我们将跳过对空间的严格定义:只需记住表达式\(x \in \mathbb{R}\)是说\(x\)是一个实值标量的正式方式。符号\(\in\)(读作“属于”)表示集合中的成员关系。例如,\(x, y \in \{0, 1\}\)表示\(x\)\(y\)是只能取值\(0\)\(1\)的变量。

标量被实现为仅包含一个元素的张量。下面,我们分配两个标量并执行常见的加法、乘法、除法和指数运算。

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.))
x = np.array(3.0)
y = np.array(2.0)

x + y, x * y, x / y, x ** y
[21:50:12] ../src/storage/storage.cc:196: Using Pooled (Naive) StorageManager for CPU
(array(5.), array(6.), array(1.5), array(9.))
x = jnp.array(3.0)
y = jnp.array(2.0)

x + y, x * y, x / y, x**y
No GPU/TPU found, falling back to CPU. (Set TF_CPP_MIN_LOG_LEVEL=0 and rerun for more info.)
(Array(5., dtype=float32, weak_type=True),
 Array(6., dtype=float32, weak_type=True),
 Array(1.5, dtype=float32, weak_type=True),
 Array(9., dtype=float32, weak_type=True))
x = tf.constant(3.0)
y = tf.constant(2.0)

x + y, x * y, x / y, x**y
(<tf.Tensor: shape=(), dtype=float32, numpy=5.0>,
 <tf.Tensor: shape=(), dtype=float32, numpy=6.0>,
 <tf.Tensor: shape=(), dtype=float32, numpy=1.5>,
 <tf.Tensor: shape=(), dtype=float32, numpy=9.0>)

2.3.2. 向量

就目前而言,你可以将向量视为一个固定长度的标量数组。与它们在代码中的对应物一样,我们称这些标量为向量的元素(同义词包括条目分量)。当向量代表来自真实世界数据集的样本时,它们的值具有现实世界的意义。例如,如果我们正在训练一个模型来预测贷款违约的风险,我们可能会将每个申请人与一个向量关联起来,其分量对应于他们的收入、就业年限或先前违约次数等量。如果我们正在研究心脏病发作的风险,每个向量可能代表一个病人,其分量可能对应于他们最近的生命体征、胆固醇水平、每天锻炼分钟数等。我们用粗体小写字母表示向量(例如,\(\mathbf{x}\)\(\mathbf{y}\)\(\mathbf{z}\))。

向量被实现为\(1\)阶张量。通常,这样的张量可以有任意长度,受内存限制。注意:在 Python 中,与大多数编程语言一样,向量索引从\(0\)开始,也称为零基索引,而在线性代数中,下标从\(1\)开始(一基索引)。

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

我们可以使用下标来引用向量的元素。例如,\(x_2\)表示\(\mathbf{x}\)的第二个元素。因为\(x_2\)是标量,所以我们不加粗。默认情况下,我们通过垂直堆叠其元素来可视化向量。

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

这里 \(x_1, \ldots, x_n\) 是向量的元素。稍后,我们将区分这种列向量和元素水平堆叠的行向量。回想一下,我们通过索引访问张量的元素。

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

为了表示一个向量包含 \(n\) 个元素,我们写成 \(\mathbf{x} \in \mathbb{R}^n\)。正式地,我们称 \(n\) 为向量的维度。在代码中,这对应于张量的长度,可以通过 Python 的内置 len 函数访问。

len(x)
3
len(x)
3
len(x)
3
len(x)
3

我们也可以通过 shape 属性访问长度。形状是一个元组,表示张量沿每个轴的长度。只有一个轴的张量的形状只有一个元素。

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

通常,“维度”这个词会被滥用,既指轴的数量,也指特定轴上的长度。为了避免这种混淆,我们用来指轴的数量,专门用维数来指分量的数量。

2.3.3. 矩阵

正如标量是\(0\)阶张量,向量是\(1\)阶张量,矩阵是\(2\)阶张量。我们用粗体大写字母表示矩阵(例如,\(\mathbf{X}\)\(\mathbf{Y}\)\(\mathbf{Z}\)),并在代码中用具有两个轴的张量表示它们。表达式\(\mathbf{A} \in \mathbb{R}^{m \times n}\)表示矩阵\(\mathbf{A}\)包含\(m \times n\)个实值标量,排列为\(m\)行和\(n\)列。当\(m = n\)时,我们称矩阵为方阵。在视觉上,我们可以将任何矩阵表示为一个表格。为了引用单个元素,我们使用行和列的下标,例如,\(a_{ij}\)是属于\(\mathbf{A}\)的第\(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}\]

在代码中,我们用形状为(\(m\), \(n\))的\(2\)阶张量来表示一个矩阵\(\mathbf{A} \in \mathbb{R}^{m \times n}\)。我们可以通过将期望的形状传递给reshape,将任何大小合适的\(m \times n\)张量转换为一个\(m \times n\)矩阵。

A = torch.arange(6).reshape(3, 2)
A
tensor([[0, 1],
        [2, 3],
        [4, 5]])
A = np.arange(6).reshape(3, 2)
A
array([[0., 1.],
       [2., 3.],
       [4., 5.]])
A = jnp.arange(6).reshape(3, 2)
A
Array([[0, 1],
       [2, 3],
       [4, 5]], dtype=int32)
A = tf.reshape(tf.range(6), (3, 2))
A
<tf.Tensor: shape=(3, 2), dtype=int32, numpy=
array([[0, 1],
       [2, 3],
       [4, 5]], dtype=int32)>

有时我们想翻转轴。当我们交换矩阵的行和列时,结果称为它的转置。形式上,我们用\(\mathbf{A}^\top\)表示矩阵\(\mathbf{A}\)的转置,如果\(\mathbf{B} = \mathbf{A}^\top\),那么对所有的\(i\)\(j\)都有\(b_{ij} = a_{ji}\)。因此,一个\(m \times n\)矩阵的转置是一个\(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
tensor([[0, 2, 4],
        [1, 3, 5]])
A.T
array([[0., 2., 4.],
       [1., 3., 5.]])
A.T
Array([[0, 2, 4],
       [1, 3, 5]], dtype=int32)
tf.transpose(A)
<tf.Tensor: shape=(2, 3), dtype=int32, numpy=
array([[0, 2, 4],
       [1, 3, 5]], dtype=int32)>

对称矩阵是等于其自身转置的方阵子集:\(\mathbf{A} = \mathbf{A}^\top\)。下面的矩阵是对称的。

A = torch.tensor([[1, 2, 3], [2, 0, 4], [3, 4, 5]])
A == A.T
tensor([[True, True, True],
        [True, True, True],
        [True, True, True]])
A = np.array([[1, 2, 3], [2, 0, 4], [3, 4, 5]])
A == A.T
array([[ True,  True,  True],
       [ True,  True,  True],
       [ True,  True,  True]])
A = jnp.array([[1, 2, 3], [2, 0, 4], [3, 4, 5]])
A == A.T
Array([[ True,  True,  True],
       [ True,  True,  True],
       [ True,  True,  True]], dtype=bool)
A = tf.constant([[1, 2, 3], [2, 0, 4], [3, 4, 5]])
A == tf.transpose(A)
<tf.Tensor: shape=(3, 3), dtype=bool, numpy=
array([[ True,  True,  True],
       [ True,  True,  True],
       [ True,  True,  True]])>

矩阵对于表示数据集非常有用。通常,行对应于单个记录,列对应于不同的属性。

2.3.4. 张量

虽然仅用标量、向量和矩阵就可以在机器学习旅程中走得很远,但最终你可能需要处理更高阶的张量。张量为我们提供了一种描述\(n\)阶数组扩展的通用方法。我们称张量类的软件对象为“张量”,正是因为它们也可以有任意数量的轴。虽然用张量这个词来指代数学对象及其在代码中的实现可能会令人困惑,但我们的意思通常可以从上下文中清楚地看出来。我们用特殊字体的大写字母表示一般张量(例如,\(\mathsf{X}\)\(\mathsf{Y}\)\(\mathsf{Z}\)),它们的索引机制(例如,\(x_{ijk}\)\([\mathsf{X}]_{1, 2i-1, 3}\))自然地遵循矩阵的索引机制。

当我们开始处理图像时,张量将变得更加重要。每张图像都以一个\(3\)阶张量的形式出现,其轴对应于高度、宽度和通道。在每个空间位置,每种颜色(红色、绿色和蓝色)的强度都沿着通道堆叠。此外,图像的集合在代码中由一个\(4\)阶张量表示,其中不同的图像沿第一个轴索引。更高阶的张量是通过增加形状分量的数量来构造的,就像向量和矩阵一样。

torch.arange(24).reshape(2, 3, 4)
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]]])
np.arange(24).reshape(2, 3, 4)
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.]]])
jnp.arange(24).reshape(2, 3, 4)
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)
tf.reshape(tf.range(24), (2, 3, 4))
<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 = torch.arange(6, dtype=torch.float32).reshape(2, 3)
B = A.clone()  # Assign a copy of A to B by allocating new memory
A, A + B
(tensor([[0., 1., 2.],
         [3., 4., 5.]]),
 tensor([[ 0.,  2.,  4.],
         [ 6.,  8., 10.]]))
A = np.arange(6).reshape(2, 3)
B = A.copy()  # Assign a copy of A to B by allocating new memory
A, A + B
(array([[0., 1., 2.],
        [3., 4., 5.]]),
 array([[ 0.,  2.,  4.],
        [ 6.,  8., 10.]]))
A = jnp.arange(6, dtype=jnp.float32).reshape(2, 3)
B = A
A, A + B
(Array([[0., 1., 2.],
        [3., 4., 5.]], dtype=float32),
 Array([[ 0.,  2.,  4.],
        [ 6.,  8., 10.]], dtype=float32))
A = tf.reshape(tf.range(6, dtype=tf.float32), (2, 3))
B = A  # No cloning of A to B by allocating new memory
A, A + B
(<tf.Tensor: shape=(2, 3), dtype=float32, numpy=
 array([[0., 1., 2.],
        [3., 4., 5.]], dtype=float32)>,
 <tf.Tensor: shape=(2, 3), dtype=float32, numpy=
 array([[ 0.,  2.,  4.],
        [ 6.,  8., 10.]], dtype=float32)>)

两个矩阵的按元素乘积称为它们的哈达玛积(记作\(\odot\))。我们可以写出两个矩阵\(\mathbf{A}, \mathbf{B} \in \mathbb{R}^{m \times n}\)的哈达玛积的元素

(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
tensor([[ 0.,  1.,  4.],
        [ 9., 16., 25.]])
A * B
array([[ 0.,  1.,  4.],
       [ 9., 16., 25.]])
A * B
Array([[ 0.,  1.,  4.],
       [ 9., 16., 25.]], dtype=float32)
A * B
<tf.Tensor: shape=(2, 3), dtype=float32, numpy=
array([[ 0.,  1.,  4.],
       [ 9., 16., 25.]], dtype=float32)>

将一个标量与一个张量相加或相乘,会产生一个与原始张量形状相同的结果。这里,张量的每个元素都与标量相加(或相乘)。

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 = 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 = jnp.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]]], dtype=int32),
 (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. 降维

通常,我们希望计算一个张量元素的总和。为了表示长度为\(n\)的向量\(\mathbf{x}\)中元素的和,我们写成\(\sum_{i=1}^n x_i\)。有一个简单的函数可以实现它

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

为了表示任意形状张量元素的和,我们只需对其所有轴求和。例如,一个\(m \times n\)矩阵\(\mathbf{A}\)的元素和可以写成\(\sum_{i=1}^{m} \sum_{j=1}^{n} a_{ij}\)

A.shape, A.sum()
(torch.Size([2, 3]), tensor(15.))
A.shape, A.sum()
((2, 3), array(15.))
A.shape, A.sum()
((2, 3), Array(15., dtype=float32))
A.shape, tf.reduce_sum(A)
(TensorShape([2, 3]), <tf.Tensor: shape=(), dtype=float32, numpy=15.0>)

默认情况下,调用求和函数会沿所有轴对张量进行降维,最终产生一个标量。我们的库也允许我们指定张量应该沿哪些轴进行降维。为了对所有行的元素求和(轴0),我们在sum中指定axis=0。由于输入矩阵沿轴0降维以生成输出向量,因此该轴在输出的形状中缺失。

A.shape, A.sum(axis=0).shape
(torch.Size([2, 3]), torch.Size([3]))
A.shape, A.sum(axis=0).shape
((2, 3), (3,))
A.shape, A.sum(axis=0).shape
((2, 3), (3,))
A.shape, tf.reduce_sum(A, axis=0).shape
(TensorShape([2, 3]), TensorShape([3]))

指定axis=1将通过对所有列的元素求和来减少列维度(轴1)。

A.shape, A.sum(axis=1).shape
(torch.Size([2, 3]), torch.Size([2]))
A.shape, A.sum(axis=1).shape
((2, 3), (2,))
A.shape, A.sum(axis=1).shape
((2, 3), (2,))
A.shape, tf.reduce_sum(A, axis=1).shape
(TensorShape([2, 3]), TensorShape([2]))

通过求和对矩阵的行和列进行降维等同于对矩阵的所有元素求和。

A.sum(axis=[0, 1]) == A.sum()  # Same as A.sum()
tensor(True)
A.sum(axis=[0, 1]) == A.sum()  # Same as A.sum()
array(True)
A.sum(axis=[0, 1]) == A.sum()  # Same as A.sum()
Array(True, dtype=bool)
tf.reduce_sum(A, axis=[0, 1]), tf.reduce_sum(A)  # Same as tf.reduce_sum(A)
(<tf.Tensor: shape=(), dtype=float32, numpy=15.0>,
 <tf.Tensor: shape=(), dtype=float32, numpy=15.0>)

一个相关的量是均值,也称为平均值。我们通过将总和除以元素总数来计算均值。因为计算均值非常普遍,所以它有一个专门的库函数,其工作方式与sum类似。

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

同样,计算均值的函数也可以沿特定轴对张量进行降维。

A.mean(axis=0), A.sum(axis=0) / A.shape[0]
(tensor([1.5000, 2.5000, 3.5000]), tensor([1.5000, 2.5000, 3.5000]))
A.mean(axis=0), A.sum(axis=0) / A.shape[0]
(array([1.5, 2.5, 3.5]), array([1.5, 2.5, 3.5]))
A.mean(axis=0), A.sum(axis=0) / A.shape[0]
(Array([1.5, 2.5, 3.5], dtype=float32), Array([1.5, 2.5, 3.5], dtype=float32))
tf.reduce_mean(A, axis=0), tf.reduce_sum(A, axis=0) / A.shape[0]
(<tf.Tensor: shape=(3,), dtype=float32, numpy=array([1.5, 2.5, 3.5], dtype=float32)>,
 <tf.Tensor: shape=(3,), dtype=float32, numpy=array([1.5, 2.5, 3.5], dtype=float32)>)

2.3.7. 非降维求和

有时,在调用计算总和或均值的函数时,保持轴数不变会很有用。当我们想要使用广播机制时,这很重要。

sum_A = A.sum(axis=1, keepdims=True)
sum_A, sum_A.shape
(tensor([[ 3.],
         [12.]]),
 torch.Size([2, 1]))
sum_A = A.sum(axis=1, keepdims=True)
sum_A, sum_A.shape
(array([[ 3.],
        [12.]]),
 (2, 1))
sum_A = A.sum(axis=1, keepdims=True)
sum_A, sum_A.shape
(Array([[ 3.],
        [12.]], dtype=float32),
 (2, 1))
sum_A = tf.reduce_sum(A, axis=1, keepdims=True)
sum_A, sum_A.shape
(<tf.Tensor: shape=(2, 1), dtype=float32, numpy=
 array([[ 3.],
        [12.]], dtype=float32)>,
 TensorShape([2, 1]))

例如,由于sum_A在对每行求和后保持其两个轴,我们可以通过广播将A除以sum_A,创建一个每行总和为\(1\)的矩阵。

A / sum_A
tensor([[0.0000, 0.3333, 0.6667],
        [0.2500, 0.3333, 0.4167]])
A / sum_A
array([[0.        , 0.33333334, 0.6666667 ],
       [0.25      , 0.33333334, 0.41666666]])
A / sum_A
Array([[0.        , 0.33333334, 0.6666667 ],
       [0.25      , 0.33333334, 0.41666666]], dtype=float32)
A / sum_A
<tf.Tensor: shape=(2, 3), dtype=float32, numpy=
array([[0.        , 0.33333334, 0.6666667 ],
       [0.25      , 0.33333334, 0.41666666]], dtype=float32)>

如果我们想计算A的元素沿某个轴(例如axis=0,即逐行)的累积和,我们可以调用cumsum函数。按照设计,此函数不会沿任何轴减少输入张量。

A.cumsum(axis=0)
tensor([[0., 1., 2.],
        [3., 5., 7.]])
A.cumsum(axis=0)
array([[0., 1., 2.],
       [3., 5., 7.]])
A.cumsum(axis=0)
Array([[0., 1., 2.],
       [3., 5., 7.]], dtype=float32)
tf.cumsum(A, axis=0)
<tf.Tensor: shape=(2, 3), dtype=float32, numpy=
array([[0., 1., 2.],
       [3., 5., 7.]], dtype=float32)>

2.3.8. 点积

到目前为止,我们只进行了按元素操作、求和和求平均值。如果这就是我们能做的一切,线性代数就不值得有自己的一节了。幸运的是,事情在这里变得更有趣了。最基本的操作之一是点积。给定两个向量\(\mathbf{x}, \mathbf{y} \in \mathbb{R}^d\),它们的点积 \(\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 = torch.ones(3, dtype = torch.float32)
x, y, torch.dot(x, y)
(tensor([0., 1., 2.]), tensor([1., 1., 1.]), tensor(3.))
y = np.ones(3)
x, y, np.dot(x, y)
(array([0., 1., 2.]), array([1., 1., 1.]), array(3.))
y = jnp.ones(3, dtype = jnp.float32)
x, y, jnp.dot(x, y)
(Array([0., 1., 2.], dtype=float32),
 Array([1., 1., 1.], dtype=float32),
 Array(3., dtype=float32))
y = tf.ones(3, dtype=tf.float32)
x, y, tf.tensordot(x, y, axes=1)
(<tf.Tensor: shape=(3,), dtype=float32, numpy=array([0., 1., 2.], dtype=float32)>,
 <tf.Tensor: shape=(3,), dtype=float32, numpy=array([1., 1., 1.], dtype=float32)>,
 <tf.Tensor: shape=(), dtype=float32, numpy=3.0>)

等效地,我们可以通过执行按元素乘法然后求和来计算两个向量的点积。

torch.sum(x * y)
tensor(3.)
np.sum(x * y)
array(3.)
jnp.sum(x * y)
Array(3., dtype=float32)
tf.reduce_sum(x * y)
<tf.Tensor: shape=(), dtype=float32, numpy=3.0>

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

2.3.9. 矩阵-向量积

现在我们知道如何计算点积了,我们可以开始理解一个\(m \times n\)矩阵\(\mathbf{A}\)和一个\(n\)维向量\(\mathbf{x}\)之间的乘积。首先,我们用它的行向量来可视化我们的矩阵

(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\)是表示矩阵\(\mathbf{A}\)\(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}\)的变换。这些变换非常有用。例如,我们可以将旋转表示为与某些方阵的乘法。矩阵-向量积还描述了在给定前一层输出的情况下,计算神经网络每一层输出所涉及的关键计算。

为了在代码中表示矩阵-向量积,我们使用mv函数。注意,A的列维度(沿轴1的长度)必须与x的维度(其长度)相同。Python有一个方便的运算符@,可以执行矩阵-向量和矩阵-矩阵乘积(取决于其参数)。因此我们可以写成A@x

A.shape, x.shape, torch.mv(A, x), A@x
(torch.Size([2, 3]), torch.Size([3]), tensor([ 5., 14.]), tensor([ 5., 14.]))

为了在代码中表示矩阵-向量积,我们使用相同的dot函数。该操作是根据参数的类型推断的。请注意,A的列维度(沿轴1的长度)必须与x的维度(其长度)相同。

A.shape, x.shape, np.dot(A, x)
((2, 3), (3,), array([ 5., 14.]))
A.shape, x.shape, jnp.matmul(A, x)
((2, 3), (3,), Array([ 5., 14.], dtype=float32))

为了在代码中表示矩阵-向量积,我们使用matvec函数。注意,A的列维度(沿轴1的长度)必须与x的维度(其长度)相同。

A.shape, x.shape, tf.linalg.matvec(A, x)
(TensorShape([2, 3]),
 TensorShape([3]),
 <tf.Tensor: shape=(2,), dtype=float32, numpy=array([ 5., 14.], dtype=float32)>)

2.3.10. 矩阵-矩阵乘法

一旦你掌握了点积和矩阵-向量积,那么矩阵-矩阵乘法就应该很简单了。

假设我们有两个矩阵 \(\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\)列的列向量:

(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}\]

为了形成矩阵乘积\(\mathbf{C} \in \mathbb{R}^{n \times m}\),我们只需计算每个元素\(c_{ij}\)作为\(\mathbf{A}\)的第\(i^{\textrm{th}}\)行和\(\mathbf{B}\)的第\(j^{\textrm{th}}\)列之间的点积,即\(\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\)个矩阵-向量积或\(m \times n\)个点积,并将结果拼接成一个\(n \times m\)的矩阵。在下面的代码片段中,我们对AB进行矩阵乘法。这里,A是一个有两行三列的矩阵,而B是一个有三行四列的矩阵。相乘之后,我们得到一个有两行四列的矩阵。

B = torch.ones(3, 4)
torch.mm(A, B), A@B
(tensor([[ 3.,  3.,  3.,  3.],
         [12., 12., 12., 12.]]),
 tensor([[ 3.,  3.,  3.,  3.],
         [12., 12., 12., 12.]]))
B = np.ones(shape=(3, 4))
np.dot(A, B)
array([[ 3.,  3.,  3.,  3.],
       [12., 12., 12., 12.]])
B = jnp.ones((3, 4))
jnp.matmul(A, B)
Array([[ 3.,  3.,  3.,  3.],
       [12., 12., 12., 12.]], dtype=float32)
B = tf.ones((3, 4), tf.float32)
tf.matmul(A, B)
<tf.Tensor: shape=(2, 4), dtype=float32, numpy=
array([[ 3.,  3.,  3.,  3.],
       [12., 12., 12., 12.]], dtype=float32)>

术语矩阵-矩阵乘法通常被简化为矩阵乘法,不应与哈达玛积混淆。

2.3.11. 范数

线性代数中最有用的一些运算符是范数。通俗地说,一个向量的范数告诉我们它有多。例如,\(\ell_2\)范数测量向量的(欧几里得)长度。这里,我们使用的是一个关于向量分量大小(而不是其维数)的大小概念。

范数是一个函数\(\| \cdot \|\),它将一个向量映射到一个标量,并满足以下三个性质:

  1. 给定任意向量\(\mathbf{x}\),如果我们将向量(的所有元素)乘以一个标量\(\alpha \in \mathbb{R}\),它的范数也会相应地缩放:

    (2.3.10)\[\|\alpha \mathbf{x}\| = |\alpha| \|\mathbf{x}\|.\]
  2. 对于任意向量\(\mathbf{x}\)\(\mathbf{y}\):范数满足三角不等式:

    (2.3.11)\[\|\mathbf{x} + \mathbf{y}\| \leq \|\mathbf{x}\| + \|\mathbf{y}\|.\]
  3. 一个向量的范数是非负的,并且只有当向量为零时才为零:

    (2.3.12)\[\|\mathbf{x}\| > 0 \textrm{ for all } \mathbf{x} \neq 0.\]

许多函数都是有效的范数,不同的范数编码了不同的大小概念。我们在小学几何中学到的,计算直角三角形斜边长的欧几里得范数,是向量元素平方和的平方根。正式地说,这被称为\(\ell_2\)范数,表示为

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

norm方法计算\(\ell_2\)范数。

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

\(\ell_1\)范数也很常见,相关的度量称为曼哈顿距离。根据定义,\(\ell_1\)范数是对向量元素绝对值的求和:

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

\(\ell_2\)范数相比,它对异常值不太敏感。为了计算\(\ell_1\)范数,我们将绝对值与求和操作组合起来。

torch.abs(u).sum()
tensor(7.)
np.abs(u).sum()
array(7.)
jnp.linalg.norm(u, ord=1) # same as jnp.abs(u).sum()
Array(7., dtype=float32)
tf.reduce_sum(tf.abs(u))
<tf.Tensor: shape=(), dtype=float32, numpy=7.0>

\(\ell_2\)\(\ell_1\)范数都是更一般的\(\ell_p\)范数的特例:

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

对于矩阵,情况更为复杂。毕竟,矩阵既可以被看作是单个条目的集合,可以被看作是作用于向量并将其转换为其他向量的对象。例如,我们可以问,相对于\(\mathbf{v}\),矩阵-向量积\(\mathbf{X} \mathbf{v}\)可能长多少。这种思路导致了所谓的谱范数。现在,我们介绍弗罗贝尼乌斯范数,它计算起来容易得多,定义为矩阵元素平方和的平方根:

(2.3.16)\[\|\mathbf{X}\|_\textrm{F} = \sqrt{\sum_{i=1}^m \sum_{j=1}^n x_{ij}^2}.\]

弗罗贝尼乌斯范数的行为就像一个矩阵形状向量的\(\ell_2\)范数。调用以下函数将计算矩阵的弗罗贝尼乌斯范数。

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

虽然我们不想讲得太超前,但我们已经可以对为什么这些概念有用建立一些直觉。在深度学习中,我们经常试图解决优化问题:最大化分配给观测数据的概率;最大化与推荐模型相关的收入;最小化预测与真实观测值之间的距离;最小化同一人照片表示之间的距离,同时最大化不同人照片表示之间的距离。这些距离,构成了深度学习算法的目标,通常表示为范数。

2.3.12. 讨论

在本节中,我们回顾了理解现代深度学习大部分内容所需的所有线性代数知识。不过,线性代数还有很多内容,其中大部分对机器学习很有用。例如,矩阵可以分解为因子,这些分解可以揭示真实世界数据集中的低维结构。有整个机器学习的子领域专注于使用矩阵分解及其对高阶张量的推广来发现数据集中的结构并解决预测问题。但这本书专注于深度学习。我们相信,一旦你亲手将机器学习应用于真实数据集,你将更倾向于学习更多的数学知识。因此,虽然我们保留稍后介绍更多数学的权利,但我们在这里结束本节。

如果你渴望学习更多线性代数,有许多优秀的图书和在线资源。对于更高级的速成课程,可以考虑查看Strang (1993)Kolter (2008)Petersen and Pedersen (2008)

总结一下:

  • 标量、向量、矩阵和张量是线性代数中使用的基本数学对象,分别具有零、一、二和任意数量的轴。

  • 张量可以通过索引或诸如summean等操作沿指定轴进行切片或降维。

  • 按元素乘积称为哈达玛积。相比之下,点积、矩阵-向量积和矩阵-矩阵积不是按元素操作,通常返回形状与操作数不同的对象。

  • 与哈达玛积相比,矩阵-矩阵积的计算时间要长得多(立方时间而非二次时间)。

  • 范数捕捉了向量(或矩阵)大小的各种概念,并通常应用于两个向量的差来测量它们之间的距离。

  • 常见的向量范数包括\(\ell_1\)\(\ell_2\)范数,常见的矩阵范数包括范数和弗罗贝尼乌斯范数。

2.3.13. 练习

  1. 证明矩阵转置的转置是矩阵本身:\((\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\)总是对称的吗?你能仅用前两个练习的结果来证明吗?

  4. 我们在本节中定义了形状为(2, 3, 4)的张量Xlen(X)的输出是什么?在不实现任何代码的情况下写下你的答案,然后用代码检查你的答案。

  5. 对于任意形状的张量Xlen(X)总是对应于X的某个轴的长度吗?是哪个轴?

  6. 运行A / A.sum(axis=1)看看会发生什么。你能分析结果吗?

  7. 在曼哈顿市中心的两个点之间旅行时,你需要覆盖的距离是多少(用坐标,即大道和街道来表示)?你能斜着走吗?

  8. 考虑一个形状为(2, 3, 4)的张量。沿轴0、1和2的求和输出形状是什么?

  9. 将一个有三个或更多轴的张量输入到linalg.norm函数并观察其输出。这个函数对任意形状的张量计算什么?

  10. 考虑三个大矩阵,比如\(\mathbf{A} \in \mathbb{R}^{2^{10} \times 2^{16}}\)\(\mathbf{B} \in \mathbb{R}^{2^{16} \times 2^{5}}\)\(\mathbf{C} \in \mathbb{R}^{2^{5} \times 2^{14}}\),用高斯随机变量初始化。你想要计算乘积\(\mathbf{A} \mathbf{B} \mathbf{C}\)。根据你是计算\((\mathbf{A} \mathbf{B}) \mathbf{C}\)还是\(\mathbf{A} (\mathbf{B} \mathbf{C})\),内存占用和速度有区别吗?为什么?

  11. 考虑三个大矩阵,比如\(\mathbf{A} \in \mathbb{R}^{2^{10} \times 2^{16}}\)\(\mathbf{B} \in \mathbb{R}^{2^{16} \times 2^{5}}\)\(\mathbf{C} \in \mathbb{R}^{2^{5} \times 2^{16}}\)。根据你是计算\(\mathbf{A} \mathbf{B}\)还是\(\mathbf{A} \mathbf{C}^\top\),速度有区别吗?为什么?如果你在不克隆内存的情况下初始化\(\mathbf{C} = \mathbf{B}^\top\),情况会有什么变化?为什么?

  12. 考虑三个矩阵,比如\(\mathbf{A}, \mathbf{B}, \mathbf{C} \in \mathbb{R}^{100 \times 200}\)。通过堆叠\([\mathbf{A}, \mathbf{B}, \mathbf{C}]\)构造一个有三个轴的张量。它的维度是多少?切出第三个轴的第二个坐标以恢复\(\mathbf{B}\)。检查你的答案是否正确。