22.1. 几何和线性代数操作¶ 在 SageMaker Studio Lab 中打开 Notebook
在 第 2.3 节中,我们学习了线性代数的基础知识,并了解了它如何用于表示转换数据的常见操作。线性代数是深度学习和更广泛的机器学习中许多工作的基础数学支柱之一。虽然 第 2.3 节 包含了足够多的工具来传达现代深度学习模型的机理,但这个主题还有很多内容。在本节中,我们将更深入地探讨,重点介绍线性代数运算的一些几何解释,并介绍一些基本概念,包括特征值和特征向量。
22.1.1. 向量的几何学¶
首先,我们需要讨论向量的两种常见几何解释,即空间中的点或方向。从根本上说,向量是一个数字列表,如下面的Python列表所示。
v = [1, 7, 0, 1]
v = [1, 7, 0, 1]
v = [1, 7, 0, 1]
数学家最常将其写成列向量或行向量,即
或
这些通常有不同的解释,其中数据样本是列向量,而用于形成加权和的权重是行向量。然而,灵活处理可能是有益的。正如我们在 第 2.3 节 中所述,虽然单个向量的默认方向是列向量,但对于任何表示表格数据集的矩阵,将每个数据样本视为矩阵中的行向量更为常规。
给定一个向量,我们应该给它的第一个解释是空间中的一个点。在二维或三维空间中,我们可以通过使用向量的分量来定义点相对于一个称为原点的固定参考点在空间中的位置来可视化这些点。这可以在 图 22.1.1 中看到。
图 22.1.1 将向量可视化为平面中点的示意图。向量的第一个分量给出 \(\mathit{x}\) 坐标,第二个分量给出 \(\mathit{y}\) 坐标。更高维度是类似的,尽管更难可视化。¶
这种几何观点使我们能够在一个更抽象的层面上考虑问题。我们不再面临一些看似无法克服的问题,比如将图片分类为猫或狗,而是可以开始将任务抽象地看作空间中点的集合,并将任务描绘成发现如何分离两个不同的点簇。
同时,人们对向量还有第二种观点:作为空间中的方向。我们不仅可以认为向量 \(\mathbf{v} = [3,2]^\top\) 是从原点向右 \(3\) 个单位、向上 \(2\) 个单位的位置,我们还可以认为它本身就是向右 \(3\) 步、向上 \(2\) 步的方向。这样,我们认为 图 22.1.2 中的所有向量都是相同的。
图 22.1.2 任何向量都可以可视化为平面中的一个箭头。在这种情况下,绘制的每个向量都是向量 \((3,2)^\top\) 的表示。¶
这种转变的好处之一是我们可以直观地理解向量加法的行为。具体来说,我们先沿着一个向量给出的方向移动,然后再沿着另一个向量给出的方向移动,如 图 22.1.3 所示。
图 22.1.3 我们可以通过先跟随一个向量,再跟随另一个向量来可视化向量加法。¶
向量减法也有类似的解释。通过考虑恒等式 \(\mathbf{u} = \mathbf{v} + (\mathbf{u}-\mathbf{v})\),我们看到向量 \(\mathbf{u}-\mathbf{v}\) 是将我们从点 \(\mathbf{v}\) 带到点 \(\mathbf{u}\) 的方向。
22.1.2. 点积和角度¶
正如我们在 第 2.3 节 中看到的,如果我们取两个列向量 \(\mathbf{u}\) 和 \(\mathbf{v}\),我们可以通过计算它们的点积
因为 (22.1.3) 是对称的,我们将模仿经典乘法的符号,写成
以强调交换向量的顺序会得到相同的结果。
点积 (22.1.3) 也有一个几何解释:它与两个向量之间的夹角密切相关。考虑 图 22.1.4 中显示的角度。
图 22.1.4 平面上任意两个向量之间都有一个明确定义的角度 \(\theta\)。我们将看到这个角度与点积密切相关。¶
首先,让我们考虑两个特定的向量
向量 \(\mathbf{v}\) 的长度为 \(r\) 并与 \(x\) 轴平行,向量 \(\mathbf{w}\) 的长度为 \(s\) 并与 \(x\) 轴成 \(\theta\) 角。如果我们计算这两个向量的点积,我们会看到
通过一些简单的代数操作,我们可以重新排列各项得到
简而言之,对于这两个特定的向量,点积结合范数告诉我们两个向量之间的夹角。这个事实在一般情况下也是成立的。我们这里不推导这个表达式,但是,如果我们考虑用两种方式写出 \(\|\mathbf{v} - \mathbf{w}\|^2\):一种是用点积,另一种是几何上使用余弦定理,我们就可以得到完整的关系。实际上,对于任意两个向量 \(\mathbf{v}\) 和 \(\mathbf{w}\),它们之间的夹角是
这是一个很好的结果,因为计算中没有任何东西涉及到二维。实际上,我们可以在三维或三百万维中使用这个公式而没有问题。
作为一个简单的例子,让我们看看如何计算一对向量之间的角度
%matplotlib inline
import torch
import torchvision
from IPython import display
from torchvision import transforms
from d2l import torch as d2l
def angle(v, w):
return torch.acos(v.dot(w) / (torch.norm(v) * torch.norm(w)))
angle(torch.tensor([0, 1, 2], dtype=torch.float32), torch.tensor([2.0, 3, 4]))
tensor(0.4190)
%matplotlib inline
from IPython import display
from mxnet import gluon, np, npx
from d2l import mxnet as d2l
npx.set_np()
def angle(v, w):
return np.arccos(v.dot(w) / (np.linalg.norm(v) * np.linalg.norm(w)))
angle(np.array([0, 1, 2]), np.array([2, 3, 4]))
[22:03:02] ../src/storage/storage.cc:196: Using Pooled (Naive) StorageManager for CPU
array(0.41899002)
%matplotlib inline
import tensorflow as tf
from IPython import display
from d2l import tensorflow as d2l
def angle(v, w):
return tf.acos(tf.tensordot(v, w, axes=1) / (tf.norm(v) * tf.norm(w)))
angle(tf.constant([0, 1, 2], dtype=tf.float32), tf.constant([2.0, 3, 4]))
<tf.Tensor: shape=(), dtype=float32, numpy=0.41899002>
我们现在不会使用它,但知道我们将把夹角为 \(\pi/2\)(或等效地 \(90^{\circ}\))的向量称为正交的,这是很有用的。通过检查上面的方程,我们看到当 \(\theta = \pi/2\) 时会发生这种情况,这与 \(\cos(\theta) = 0\) 相同。这唯一可能发生的方式是点积本身为零,因此两个向量是正交的当且仅当 \(\mathbf{v}\cdot\mathbf{w} = 0\)。在几何上理解对象时,这将证明是一个有用的公式。
我们有理由问:计算角度有什么用?答案在于我们期望数据具有的不变性。考虑一张图片和一张副本图片,其中每个像素值都相同,但亮度只有原来的 \(10\%\)。单个像素的值通常与原始值相差甚远。因此,如果计算原始图像和较暗图像之间的距离,距离可能会很大。然而,对于大多数机器学习应用来说,内容是相同的——就猫/狗分类器而言,它仍然是一张猫的图片。但是,如果我们考虑角度,不难看出对于任何向量 \(\mathbf{v}\),\(\mathbf{v}\) 和 \(0.1\cdot\mathbf{v}\) 之间的角度为零。这对应于缩放向量保持方向不变而只改变长度的事实。角度认为较暗的图像是相同的。
这样的例子无处不在。在文本中,如果我们写一篇篇幅是原来两倍但内容相同的文档,我们可能希望讨论的主题不发生改变。对于某些编码(例如计算词汇表中单词出现的次数),这对应于编码文档的向量加倍,所以我们同样可以使用角度。
22.1.3. 超平面¶
除了处理向量,另一个要深入理解线性代数的关键对象是超平面,它是线(二维)或平面(三维)在高维度的推广。在一个 \(d\) 维向量空间中,一个超平面有 \(d-1\) 维,并将空间分成两个半空间。
让我们从一个例子开始。假设我们有一个列向量 \(\mathbf{w}=[2,1]^\top\)。我们想知道,“哪些点 \(\mathbf{v}\) 满足 \(\mathbf{w}\cdot\mathbf{v} = 1\)?” 回顾上面 (22.1.8) 中点积和角度的联系,我们可以看到这等价于
图 22.1.5 回顾三角学,我们看到公式 \(\|\mathbf{v}\|\cos(\theta)\) 是向量 \(\mathbf{v}\) 在 \(\mathbf{w}\) 方向上投影的长度¶
如果我们考虑这个表达式的几何意义,我们看到这等价于说 \(\mathbf{v}\) 在 \(\mathbf{w}\) 方向上的投影长度正好是 \(1/\|\mathbf{w}\|\),如 图 22.1.5 所示。所有满足这个条件的点的集合是一条与向量 \(\mathbf{w}\) 成直角的线。如果我们愿意,我们可以找到这条线的方程,发现它是 \(2x + y = 1\) 或等价地 \(y = 1 - 2x\)。
如果我们现在看看当询问满足 \(\mathbf{w}\cdot\mathbf{v} > 1\) 或 \(\mathbf{w}\cdot\mathbf{v} < 1\) 的点集时会发生什么,我们可以看到这些是投影长度分别大于或小于 \(1/\|\mathbf{w}\|\) 的情况。因此,这两个不等式定义了线的两侧。通过这种方式,我们找到了一种将我们的空间切成两半的方法,其中一侧所有点的点积都低于一个阈值,另一侧则高于,正如我们在 图 22.1.6 中看到的那样。
图 22.1.6 如果我们现在考虑表达式的不等式版本,我们看到我们的超平面(在这种情况下:只是一条线)将空间分成两半。¶
高维的情况与此大同小异。如果我们现在取 \(\mathbf{w} = [1,2,3]^\top\) 并询问三维空间中满足 \(\mathbf{w}\cdot\mathbf{v} = 1\) 的点,我们会得到一个与给定向量 \(\mathbf{w}\) 成直角的平面。这两个不等式再次定义了平面的两侧,如 图 22.1.7 所示。
图 22.1.7 任何维度的超平面都将空间分成两半。¶
虽然我们的可视化能力到此为止,但没有什么能阻止我们在数十、数百或数十亿维度中这样做。在思考机器学习模型时,这种情况经常发生。例如,我们可以将线性分类模型,比如 第 4.1 节 中的模型,理解为寻找分离不同目标类别的超平面的方法。在这种情况下,这样的超平面通常被称为决策平面。大多数深度学习分类模型都以一个线性层输入到 softmax 结束,因此可以解释为深度神经网络的作用是找到一个非线性嵌入,使得目标类别可以被超平面清晰地分离。
举一个手工构建的例子,请注意,我们可以通过取 Fashion-MNIST 数据集(见 第 4.2 节)中T恤和裤子平均值之间的向量来定义决策平面,并目测一个粗略的阈值,从而生成一个合理的模型来分类这些小图像。首先,我们将加载数据并计算平均值。
# Load in the dataset
trans = []
trans.append(transforms.ToTensor())
trans = transforms.Compose(trans)
train = torchvision.datasets.FashionMNIST(root="../data", transform=trans,
train=True, download=True)
test = torchvision.datasets.FashionMNIST(root="../data", transform=trans,
train=False, download=True)
X_train_0 = torch.stack(
[x[0] * 256 for x in train if x[1] == 0]).type(torch.float32)
X_train_1 = torch.stack(
[x[0] * 256 for x in train if x[1] == 1]).type(torch.float32)
X_test = torch.stack(
[x[0] * 256 for x in test if x[1] == 0 or x[1] == 1]).type(torch.float32)
y_test = torch.stack([torch.tensor(x[1]) for x in test
if x[1] == 0 or x[1] == 1]).type(torch.float32)
# Compute averages
ave_0 = torch.mean(X_train_0, axis=0)
ave_1 = torch.mean(X_train_1, axis=0)
# Load in the dataset
train = gluon.data.vision.FashionMNIST(train=True)
test = gluon.data.vision.FashionMNIST(train=False)
X_train_0 = np.stack([x[0] for x in train if x[1] == 0]).astype(float)
X_train_1 = np.stack([x[0] for x in train if x[1] == 1]).astype(float)
X_test = np.stack(
[x[0] for x in test if x[1] == 0 or x[1] == 1]).astype(float)
y_test = np.stack(
[x[1] for x in test if x[1] == 0 or x[1] == 1]).astype(float)
# Compute averages
ave_0 = np.mean(X_train_0, axis=0)
ave_1 = np.mean(X_train_1, axis=0)
# Load in the dataset
((train_images, train_labels), (
test_images, test_labels)) = tf.keras.datasets.fashion_mnist.load_data()
X_train_0 = tf.cast(tf.stack(train_images[[i for i, label in enumerate(
train_labels) if label == 0]] * 256), dtype=tf.float32)
X_train_1 = tf.cast(tf.stack(train_images[[i for i, label in enumerate(
train_labels) if label == 1]] * 256), dtype=tf.float32)
X_test = tf.cast(tf.stack(test_images[[i for i, label in enumerate(
test_labels) if label == 0]] * 256), dtype=tf.float32)
y_test = tf.cast(tf.stack(test_images[[i for i, label in enumerate(
test_labels) if label == 1]] * 256), dtype=tf.float32)
# Compute averages
ave_0 = tf.reduce_mean(X_train_0, axis=0)
ave_1 = tf.reduce_mean(X_train_1, axis=0)
详细检查这些平均值可能很有启发性,所以让我们绘制出它们的样子。在这种情况下,我们看到平均值确实像一件T恤的模糊图像。
# Plot average t-shirt
d2l.set_figsize()
d2l.plt.imshow(ave_0.reshape(28, 28).tolist(), cmap='Greys')
d2l.plt.show()
# Plot average t-shirt
d2l.set_figsize()
d2l.plt.imshow(ave_0.reshape(28, 28).tolist(), cmap='Greys')
d2l.plt.show()
# Plot average t-shirt
d2l.set_figsize()
d2l.plt.imshow(tf.reshape(ave_0, (28, 28)), cmap='Greys')
d2l.plt.show()
在第二种情况下,我们再次看到平均值像一条裤子的模糊图像。
# Plot average trousers
d2l.plt.imshow(ave_1.reshape(28, 28).tolist(), cmap='Greys')
d2l.plt.show()
# Plot average trousers
d2l.plt.imshow(ave_1.reshape(28, 28).tolist(), cmap='Greys')
d2l.plt.show()
# Plot average trousers
d2l.plt.imshow(tf.reshape(ave_1, (28, 28)), cmap='Greys')
d2l.plt.show()
在一个完全由机器学习得到的解决方案中,我们将从数据集中学习阈值。在这种情况下,我只是手动目测了一个在训练数据上看起来不错的阈值。
# Print test set accuracy with eyeballed threshold
w = (ave_1 - ave_0).T
# '@' is Matrix Multiplication operator in pytorch.
predictions = X_test.reshape(2000, -1) @ (w.flatten()) > -1500000
# Accuracy
torch.mean((predictions.type(y_test.dtype) == y_test).float(), dtype=torch.float64)
tensor(0.7870, dtype=torch.float64)
# Print test set accuracy with eyeballed threshold
w = (ave_1 - ave_0).T
predictions = X_test.reshape(2000, -1).dot(w.flatten()) > -1500000
# Accuracy
np.mean(predictions.astype(y_test.dtype) == y_test, dtype=np.float64)
array(0.801, dtype=float64)
# Print test set accuracy with eyeballed threshold
w = tf.transpose(ave_1 - ave_0)
predictions = tf.reduce_sum(X_test * tf.nest.flatten(w), axis=0) > -1500000
# Accuracy
tf.reduce_mean(
tf.cast(tf.cast(predictions, y_test.dtype) == y_test, tf.float32))
<tf.Tensor: shape=(), dtype=float32, numpy=0.4602704>
22.1.4. 线性变换的几何¶
通过 第 2.3 节 和上面的讨论,我们对向量、长度和角度的几何学有了扎实的理解。然而,我们忽略了一个重要的对象,那就是对由矩阵表示的线性变换的几何理解。要完全内化矩阵如何将数据在两个可能不同的高维空间之间进行转换,需要大量的练习,这超出了本附录的范围。但是,我们可以在二维空间中开始建立直觉。
假设我们有一个矩阵
如果我们想将其应用于任意向量 \(\mathbf{v} = [x, y]^\top\),我们进行乘法运算,看到
这可能看起来像一个奇怪的计算,一个清晰的东西变得有些难以理解。然而,它告诉我们,我们可以用矩阵如何变换*两个特定的向量*:\([1,0]^\top\) 和 \([0,1]^\top\) 来描述它如何变换*任何*向量。这值得我们思考片刻。我们基本上将一个无限的问题(任何一对实数会发生什么)简化为一个有限的问题(这些特定的向量会发生什么)。这些向量是一个*基*的例子,我们可以将空间中的任何向量写成这些*基向量*的加权和。
让我们画出当我们使用特定矩阵时会发生什么
如果我们看特定的向量 \(\mathbf{v} = [2, -1]^\top\),我们看到它是 \(2\cdot[1,0]^\top + -1\cdot[0,1]^\top\),因此我们知道矩阵 \(A\) 会将它发送到 \(2(\mathbf{A}[1,0]^\top) + -1(\mathbf{A}[0,1])^\top = 2[1, -1]^\top - [2,3]^\top = [0, -5]^\top\)。如果我们仔细地遵循这个逻辑,比如说考虑所有整数点对的网格,我们会看到矩阵乘法可以使网格倾斜、旋转和缩放,但网格结构必须保持,正如你在 图 22.1.8 中看到的那样。
图 22.1.8 矩阵 \(\mathbf{A}\) 作用于给定的基向量。注意整个网格是如何随之移动的。¶
这是关于矩阵表示的线性变换最需要内化的直观要点。矩阵无法将空间的不同部分以不同方式扭曲。它们所能做的就是获取我们空间中的原始坐标,并对它们进行倾斜、旋转和缩放。
有些扭曲可能很严重。例如矩阵
将整个二维平面压缩到一条直线上。识别和处理这种变换是后面一节的主题,但从几何上看,我们可以看到这与我们上面看到的变换类型有根本的不同。例如,矩阵 \(\mathbf{A}\) 的结果可以“弯曲”回原始网格。矩阵 \(\mathbf{B}\) 的结果则不能,因为我们永远不会知道向量 \([1,2]^\top\) 来自哪里——是 \([1,1]^\top\) 还是 \([0, -1]^\top\)?
虽然这张图是针对 \(2\times2\) 矩阵的,但没有什么能阻止我们将学到的经验应用到更高维度。如果我们取类似的基向量,如 \([1,0, \ldots,0]\),看看我们的矩阵将它们发送到哪里,我们就可以开始感受到矩阵乘法是如何在我们处理的任何维度空间中扭曲整个空间的。
22.1.5. 线性相关性¶
再次考虑矩阵
这将整个平面压缩到直线 \(y = 2x\) 上。现在问题来了:我们能仅仅通过观察矩阵本身来检测到这一点吗?答案是肯定的。让我们取 \(\mathbf{b}_1 = [2,4]^\top\) 和 \(\mathbf{b}_2 = [-1, -2]^\top\) 作为 \(\mathbf{B}\) 的两列。记住,我们可以将矩阵 \(\mathbf{B}\) 变换的所有东西写成矩阵列的加权和:如 \(a_1\mathbf{b}_1 + a_2\mathbf{b}_2\)。我们称之为线性组合。事实上,\(\mathbf{b}_1 = -2\cdot\mathbf{b}_2\) 意味着我们可以完全用 \(\mathbf{b}_2\) 来写出这两个列的任何线性组合,因为
这意味着其中一列在某种意义上是多余的,因为它没有定义空间中的一个独特方向。这不应该让我们太惊讶,因为我们已经看到这个矩阵将整个平面压缩成一条线。此外,我们看到线性相关性 \(\mathbf{b}_1 = -2\cdot\mathbf{b}_2\) 捕捉到了这一点。为了使这两个向量之间的关系更对称,我们将它写成
一般来说,如果存在系数 \(a_1, \ldots, a_k\) *不全为零*,使得
我们就说向量集合 \(\mathbf{v}_1, \ldots, \mathbf{v}_k\) 是线性相关的。在这种情况下,我们可以用其他向量的某种组合来表示其中一个向量,从而有效地使其变得多余。因此,矩阵列中的线性相关性证明了我们的矩阵正在将空间压缩到某个更低的维度。如果没有线性相关性,我们就说向量是线性无关的。如果矩阵的列是线性无关的,就不会发生压缩,操作可以被撤销。
22.1.6. 秩¶
如果我们有一个通用的 \(n\times m\) 矩阵,我们有理由问矩阵映射到的空间维度是多少。一个称为秩的概念将是我们的答案。在上一节中,我们注意到线性相关性证明了空间被压缩到更低的维度,所以我们将能够用它来定义秩的概念。具体来说,矩阵 \(\mathbf{A}\) 的秩是所有列子集中线性无关列的最大数量。例如,矩阵
的秩为 \(\textrm{rank}(B)=1\),因为这两列是线性相关的,但任何一列本身都不是线性相关的。对于一个更具挑战性的例子,我们可以考虑
并证明 \(\mathbf{C}\) 的秩为2,因为例如前两列是线性无关的,但是四个三列的集合中的任何一个都是相关的。
如上所述,这个过程非常低效。它需要查看我们给定矩阵的每个列子集,因此可能在列数上是指数级的。稍后我们将看到一种计算上更有效的方法来计算矩阵的秩,但现在,这足以让我们看到这个概念是明确定义的并理解其含义。
22.1.7. 可逆性¶
我们上面已经看到,与具有线性相关列的矩阵相乘是不可逆的,也就是说,没有逆操作可以总是恢复输入。然而,与一个满秩矩阵相乘(即,某个秩为 \(n\) 的 \(n \times n\) 矩阵 \(\mathbf{A}\)),我们应该总是能够撤销它。考虑矩阵
这是一个对角线上为1,其他地方为0的矩阵。我们称之为单位矩阵。它是一个在应用时保持数据不变的矩阵。要找到一个可以撤销我们矩阵 \(\mathbf{A}\) 所做操作的矩阵,我们想要找到一个矩阵 \(\mathbf{A}^{-1}\) 使得
如果我们把它看作一个系统,我们有 \(n \times n\) 个未知数(\(\mathbf{A}^{-1}\) 的元素)和 \(n \times n\) 个方程(乘积 \(\mathbf{A}^{-1}\mathbf{A}\) 的每个元素和 \(\mathbf{I}\) 的每个元素之间需要满足的等式),所以我们通常期望存在一个解。实际上,在下一节中我们将看到一个称为行列式的量,它具有只要行列式不为零,我们就能找到解的性质。我们称这样的矩阵 \(\mathbf{A}^{-1}\) 为逆矩阵。例如,如果 \(\mathbf{A}\) 是一个通用的 \(2 \times 2\) 矩阵
那么我们可以看到逆是
我们可以通过乘以由上述公式给出的逆矩阵在实践中是否有效来进行测试。
M = torch.tensor([[1, 2], [1, 4]], dtype=torch.float32)
M_inv = torch.tensor([[2, -1], [-0.5, 0.5]])
M_inv @ M
tensor([[1., 0.],
[0., 1.]])
M = np.array([[1, 2], [1, 4]])
M_inv = np.array([[2, -1], [-0.5, 0.5]])
M_inv.dot(M)
array([[1., 0.],
[0., 1.]])
M = tf.constant([[1, 2], [1, 4]], dtype=tf.float32)
M_inv = tf.constant([[2, -1], [-0.5, 0.5]])
tf.matmul(M_inv, M)
<tf.Tensor: shape=(2, 2), dtype=float32, numpy=
array([[1., 0.],
[0., 1.]], dtype=float32)>
22.1.7.1. 数值问题¶
虽然矩阵的逆在理论上很有用,但我们必须说,在实践中,我们大多数时候不希望使用矩阵的逆来解决问题。总的来说,对于求解线性方程如
存在比计算逆矩阵并乘以得到
的算法要数值稳定得多。就像除以一个小数可能导致数值不稳定一样,对一个接近低秩的矩阵求逆也可能如此。
此外,矩阵 \(\mathbf{A}\) 通常是稀疏的,也就是说它只包含少量非零值。如果我们去探索例子,我们会发现这并不意味着逆矩阵是稀疏的。即使 \(\mathbf{A}\) 是一个只有 \(5\) 百万个非零项的 \(1\) 百万乘 \(1\) 百万的矩阵(因此我们只需要存储那 \(5\) 百万个),逆矩阵通常几乎每个元素都是非负的,需要我们存储所有 \(1\textrm{M}^2\) 个元素——也就是 \(1\) 万亿个元素!
虽然我们没有时间深入探讨在使用线性代数时经常遇到的棘手的数值问题,但我们希望为您提供一些关于何时谨慎行事的直觉,并且在实践中通常避免求逆是一个很好的经验法则。
22.1.8. 行列式¶
线性代数的几何观点为解释一个称为行列式的基本量提供了一种直观的方式。再次考虑之前的网格图像,但这次有一个高亮区域(图 22.1.9)。
图 22.1.9 矩阵 \(\mathbf{A}\) 再次扭曲网格。这次,我想特别注意高亮的正方形发生了什么。¶
看那个高亮的正方形。这是一个由 \((0, 1)\) 和 \((1, 0)\) 构成的边,因此面积为1。在 \(\mathbf{A}\) 变换这个正方形后,我们看到它变成了一个平行四边形。没有理由这个平行四边形的面积应该和我们开始时一样,实际上在这里显示的特定情况下
这是一个坐标几何的练习,计算这个平行四边形的面积并得到面积是 \(5\)。
一般来说,如果我们有一个矩阵
经过一些计算,我们可以看到得到的平行四边形的面积是 \(ad-bc\)。这个面积被称为行列式。
让我们用一些示例代码快速检查一下。
torch.det(torch.tensor([[1, -1], [2, 3]], dtype=torch.float32))
tensor(5.)
import numpy as np
np.linalg.det(np.array([[1, -1], [2, 3]]))
5.000000000000001
tf.linalg.det(tf.constant([[1, -1], [2, 3]], dtype=tf.float32))
<tf.Tensor: shape=(), dtype=float32, numpy=5.0>
眼尖的人会注意到这个表达式可以是零甚至是负数。对于负数项,这是数学中普遍采用的惯例:如果矩阵翻转了图形,我们说面积被取负。现在让我们看看当行列式为零时,我们能学到什么。
让我们考虑
如果我们计算这个矩阵的行列式,我们得到 \(2\cdot(-2 ) - 4\cdot(-1) = 0\)。根据我们上面的理解,这是有道理的。\(\mathbf{B}\) 将原始图像中的正方形压缩成一个线段,其面积为零。而且,被压缩到更低的维度空间是变换后面积为零的唯一方式。因此我们看到以下结果是正确的:一个矩阵 \(A\) 是可逆的当且仅当其行列式不等于零。
作为最后的评论,想象一下我们在平面上画了任何一个图形。像计算机科学家一样思考,我们可以将该图形分解为一系列小正方形的集合,这样图形的面积本质上就是分解中正方形的数量。如果我们现在用一个矩阵来变换这个图形,我们会将这些正方形中的每一个都变成一个平行四边形,其中每一个的面积都由行列式给出。我们看到,对于任何图形,行列式给出了一个矩阵缩放任何图形面积的(有符号)数值。
计算更大矩阵的行列式可能很费力,但直觉是相同的。行列式仍然是 \(n\times n\) 矩阵缩放 \(n\) 维体积的因子。
22.1.9. 张量和常见的线性代数运算¶
在 第 2.3 节 中介绍了张量的概念。在本节中,我们将更深入地探讨张量收缩(张量等价于矩阵乘法),并看看它如何为许多矩阵和向量运算提供一个统一的视角。
对于矩阵和向量,我们知道如何将它们相乘以变换数据。如果张量要对我们有用,我们也需要为它们定义类似的乘法。考虑矩阵乘法
或等价地
这种模式我们可以为张量重复。对于张量,没有一种可以普遍选择的求和索引,所以我们需要明确指定我们想要对哪些索引求和。例如,我们可以考虑
这样的变换被称为张量收缩。它可以表示一个比单独的矩阵乘法更灵活的变换族。
作为一个常用的符号简化,我们可以注意到求和是针对表达式中出现多次的那些索引,因此人们经常使用爱因斯坦表示法,其中求和是隐式地对所有重复的索引进行的。这给出了紧凑的表达式
22.1.9.1. 线性代数中的常见例子¶
让我们看看我们之前看到的许多线性代数定义如何用这种压缩的张量表示法来表示
\(\mathbf{v} \cdot \mathbf{w} = \sum_i v_iw_i\)
\(\|\mathbf{v}\|_2^{2} = \sum_i v_iv_i\)
\((\mathbf{A}\mathbf{v})_i = \sum_j a_{ij}v_j\)
\((\mathbf{A}\mathbf{B})_{ik} = \sum_j a_{ij}b_{jk}\)
\(\textrm{tr}(\mathbf{A}) = \sum_i a_{ii}\)
通过这种方式,我们可以用简短的张量表达式替换无数专门的符号。
22.1.9.2. 在代码中表达¶
张量也可以在代码中灵活地操作。如 第 2.3 节 所示,我们可以如下创建张量。
# Define tensors
B = torch.tensor([[[1, 2, 3], [4, 5, 6]], [[7, 8, 9], [10, 11, 12]]])
A = torch.tensor([[1, 2], [3, 4]])
v = torch.tensor([1, 2])
# Print out the shapes
A.shape, B.shape, v.shape
(torch.Size([2, 2]), torch.Size([2, 2, 3]), torch.Size([2]))
# Define tensors
B = np.array([[[1, 2, 3], [4, 5, 6]], [[7, 8, 9], [10, 11, 12]]])
A = np.array([[1, 2], [3, 4]])
v = np.array([1, 2])
# Print out the shapes
A.shape, B.shape, v.shape
((2, 2), (2, 2, 3), (2,))
# Define tensors
B = tf.constant([[[1, 2, 3], [4, 5, 6]], [[7, 8, 9], [10, 11, 12]]])
A = tf.constant([[1, 2], [3, 4]])
v = tf.constant([1, 2])
# Print out the shapes
A.shape, B.shape, v.shape
(TensorShape([2, 2]), TensorShape([2, 2, 3]), TensorShape([2]))
爱因斯坦求和已被直接实现。爱因斯坦求和中出现的索引可以作为一个字符串传递,后面跟着被作用的张量。例如,要实现矩阵乘法,我们可以考虑上面看到的爱因斯坦求和(\(\mathbf{A}\mathbf{v} = a_{ij}v_j\)),并去掉索引本身来得到实现
# Reimplement matrix multiplication
torch.einsum("ij, j -> i", A, v), A@v
(tensor([ 5, 11]), tensor([ 5, 11]))
# Reimplement matrix multiplication
np.einsum("ij, j -> i", A, v), A.dot(v)
(array([ 5, 11]), array([ 5, 11]))
# Reimplement matrix multiplication
tf.einsum("ij, j -> i", A, v), tf.matmul(A, tf.reshape(v, (2, 1)))
(<tf.Tensor: shape=(2,), dtype=int32, numpy=array([ 5, 11], dtype=int32)>,
<tf.Tensor: shape=(2, 1), dtype=int32, numpy=
array([[ 5],
[11]], dtype=int32)>)
这是一个非常灵活的表示法。例如,如果我们想计算传统上写为
的内容,可以通过爱因斯坦求和实现为
torch.einsum("ijk, il, j -> kl", B, A, v)
tensor([[ 90, 126],
[102, 144],
[114, 162]])
np.einsum("ijk, il, j -> kl", B, A, v)
array([[ 90, 126],
[102, 144],
[114, 162]])
tf.einsum("ijk, il, j -> kl", B, A, v)
<tf.Tensor: shape=(3, 2), dtype=int32, numpy=
array([[ 90, 126],
[102, 144],
[114, 162]], dtype=int32)>
这种表示法对人类来说可读性强且高效,但如果出于某种原因我们需要以编程方式生成张量收缩,则会显得笨重。因此,einsum
提供了一种替代表示法,即为每个张量提供整数索引。例如,相同的张量收缩也可以写成
# PyTorch does not support this type of notation.
np.einsum(B, [0, 1, 2], A, [0, 3], v, [1], [2, 3])
array([[ 90, 126],
[102, 144],
[114, 162]])
# TensorFlow does not support this type of notation.
任何一种表示法都允许在代码中简洁高效地表示张量收缩。
22.1.10. 总结¶
向量可以在几何上解释为空间中的点或方向。
点积将角度的概念定义到任意高维空间。
超平面是直线和平面在高维的推广。它们可以用来定义决策平面,这通常用作分类任务的最后一步。
矩阵乘法可以在几何上解释为对底层坐标的均匀扭曲。它们代表了一种非常受限但数学上清晰的变换向量的方式。
线性相关性是一种判断一组向量是否位于比我们预期的更低维度空间的方法(比如你有 \(3\) 个向量生活在一个 \(2\) 维空间中)。矩阵的秩是其列中线性无关的最大子集的大小。
当矩阵的逆被定义时,矩阵求逆允许我们找到另一个矩阵来撤销第一个矩阵的作用。矩阵求逆在理论上很有用,但在实践中由于数值不稳定性需要谨慎使用。
行列式允许我们测量一个矩阵扩展或收缩一个空间的程度。非零行列式意味着一个可逆(非奇异)矩阵,而零值行列式意味着矩阵不可逆(奇异)。
张量收缩和爱因斯坦求和为表达机器学习中看到的许多计算提供了一种简洁明了的表示法。
22.1.11. 练习¶
下面两个向量之间的夹角是多少?
(22.1.35)¶\[\begin{split}\vec v_1 = \begin{bmatrix} 1 \\ 0 \\ -1 \\ 2 \end{bmatrix}, \qquad \vec v_2 = \begin{bmatrix} 3 \\ 1 \\ 0 \\ 1 \end{bmatrix}?\end{split}\]正确还是错误:\(\begin{bmatrix}1 & 2\\0&1\end{bmatrix}\) 和 \(\begin{bmatrix}1 & -2\\0&1\end{bmatrix}\) 互为逆矩阵?
假设我们在平面上画一个面积为 \(100\textrm{m}^2\) 的图形。用矩阵
(22.1.36)¶\[\begin{split}\begin{bmatrix} 2 & 3\\ 1 & 2 \end{bmatrix}.\end{split}\]变换该图形后的面积是多少?
\(\left\{\begin{pmatrix}1\\0\\-1\end{pmatrix}, \begin{pmatrix}2\\1\\-1\end{pmatrix}, \begin{pmatrix}3\\1\\1\end{pmatrix}\right\}\)
\(\left\{\begin{pmatrix}3\\1\\1\end{pmatrix}, \begin{pmatrix}1\\1\\1\end{pmatrix}, \begin{pmatrix}0\\0\\0\end{pmatrix}\right\}\)
\(\left\{\begin{pmatrix}1\\1\\0\end{pmatrix}, \begin{pmatrix}0\\1\\-1\end{pmatrix}, \begin{pmatrix}1\\0\\1\end{pmatrix}\right\}\)
假设你有一个矩阵写成 \(A = \begin{bmatrix}c\\d\end{bmatrix}\cdot\begin{bmatrix}a & b\end{bmatrix}\) 对于某些值 \(a, b, c\) 和 \(d\)。正确还是错误:这样一个矩阵的行列式总是 \(0\)?
向量 \(e_1 = \begin{bmatrix}1\\0\end{bmatrix}\) 和 \(e_2 = \begin{bmatrix}0\\1\end{bmatrix}\) 是正交的。矩阵 \(A\) 满足什么条件才能使 \(Ae_1\) 和 \(Ae_2\) 正交?
对于任意矩阵 \(A\),如何用爱因斯坦表示法写出 \(\textrm{tr}(\mathbf{A}^4)\)?