线性代数 [ 矩阵与线性变化 ]

线性代数

四个数字,为什么能够描述整个平面的变化?

现在给大家一个矩阵:

这四个数字到底在描述什么?如果我们只记住"第一行乘输入向量,得到第一个分量",当然也能算出结果。但这样一来,矩阵为什么这么乘、结果对应怎样的空间变化,就很容易变成需要另外记忆的内容。

这一篇,我们换个观察顺序:先看空间怎样变化,再看两个基向量去了哪里,最后把这些信息写成矩阵。 理解了这个过程,矩阵向量乘法就能和上一篇的线性组合联系起来。

本篇重点讨论的概念,就是线性变换与它的矩阵表示。

文中的代码、配图和计算说明为配套补充。当前主要讨论实数二维向量之间的变换,输入和输出都使用固定的标准坐标系。

变换,就是把一个向量对应到另一个向量

先把它看成一个函数

我们先不考虑矩阵。一个变换,本质上就是一个函数:给它一个输入,它按照确定的规则返回一个输出。

在当前讨论中,输入和输出都是向量。我们把变换记为 ,写成:

这里的 表示所有实数二维向量组成的空间。 表示向量 经过这个变换以后得到的结果。

比如一个向量原来指向 ,经过变换以后指向 ,我们可以把它想成:这个向量的终点,从原来的位置移动到了新的位置。

不过,一个输入向量的去向,并不能说明整个变换。 我们还要知道,同一套规则作用到其他向量时,会发生什么。

为什么课程里要画一整张网格?

如果把所有向量都画成从原点出发的箭头,画面很快就会挤满。所以这里继续使用上一篇的方法:用箭头的终点来代表向量。

接下来,选取一组水平、竖直的网格线,让上面的每一个点都按照同一规则移动。这样就能够观察到,整个空间是在旋转、拉伸、剪切,还是发生了更复杂的弯曲。

后面的配图会保留淡色的原始网格,再用彩色网格表示变换后的结果。背景坐标轴用于读取位置,彩色网格则记录原来那些点去了哪里。 网格只画出有限范围,真正的变换规则作用于整个平面。

什么样的变换,才是线性变换?

先建立图像,再补上正式条件

从几何上观察,线性变换保持原点不动;一条直线经过变换以后,不会弯成曲线。同一族平行、等距的网格线,变换后仍呈现规则的平行、等距结构,发生压缩退化时则可能重合。

这里的"等距"不是指长度保持不变,而是说原来等间隔排列的点或网格,经过变换后,仍按照一致的步长排列。线性变换可以改变长度,也可以改变夹角。

这些图像帮助我们识别现象。真正用来计算和推导的定义,是下面两条:

它们要求对所有输入向量 和所有实数 都成立。第一条表示保持向量加法,第二条表示保持数乘。正式定义可以对照教材 Interactive Linear Algebra:Linear Transformations 阅读。

把两条规则放在一起,就得到本篇最重要的关系:

先线性组合,再做变换;或者先分别变换,再使用原来的系数组合,结果相同。 后面为什么只需要追踪基向量,就是由这条关系决定的。

原点不动,是必要条件,还不是全部条件

令数乘系数为零,就能得到 。因此,线性变换一定把零向量映射到零向量。

如果把整个平面向右平移 、向上平移 ,原点就到了 。这种变换虽然让直线保持平直,但它不满足当前向量空间中的线性变换定义。

这个平移可以写成:

还有一种情况,原点没有移动,变换却仍然不线性。比如:

原点确实还在原点,但水平直线会弯曲。取 ,就能直接检查出:,而 ,数乘关系不成立。

图中左边的剪切是线性变换;中间的平移、右边的弯曲都不是。检查时,既要观察原点,也要观察整条直线和间隔,不能只挑几个看起来没有问题的位置。

还有一个边界需要补充:线性变换允许把直线压缩成一个点。 比如把所有二维向量都变成零向量,完全符合加法和数乘规则。因此,"直线保持平直"的图像,也需要包含这种退化情况。

只记录两个基向量,为什么就够了?

任意向量,先写成标准基的线性组合

前面在《线性组合·张成的空间与基》中,我们已经知道:

它们构成二维空间的标准基。任意向量都可以写成:

现在对它做线性变换,按照前面的规则:

大家重点看这里: 两个系数没有改变,改变的是它们所缩放的向量。原来缩放 ​,现在缩放 。

因此,只要知道两个标准基向量的像,也就是它们变换后的结果,就能够算出任意输入向量的像。

这里有一个前提:我们已经知道 是线性变换。 如果只是随意给出两个输入点的输出,而没有线性条件,就不能推断其他点的去向。

完整算一遍

假设两个基向量分别变到了:

我们想知道 经过变换后会到哪里。先把输入写成 ​,再保持这组组合系数:

图中绿色、黄色箭头分别对应两个标准基向量及其变换结果,蓝色箭头对应输入向量与输出向量。虚线标出了使用相同系数、先缩放再相加的路径。

我们并不需要先观察所有网格点的运动,再去读取蓝色箭头的终点。知道两根基向量的去向,按照原系数组合,就已经确定了答案。

矩阵的两列,就是两个基向量变换后的坐标

把前面的信息,按列放在一起

我们把 放在第一列,把 放在第二列,就得到:

这就是开头的矩阵。第一列告诉我们 变到了 ;第二列告诉我们 变到了 。

注意这里读取的是列 。第一行 表示两个输入分量怎样共同贡献到输出的第一个分量,它不是第一个基向量的像。

更一般地,如果:

那么矩阵向量乘法就是:

按行计算输出分量,按列理解基向量的去向。 两种看法得到的是同一个乘法规则,不需要把它们分开记成两套知识。

对于当前例子,任意输入 都会变成 。四个数字之所以足够,是因为二维输入只有两个标准基向量,而每个像又需要两个坐标描述。

使用 Python 构造矩阵,再计算结果

接下来,我们把这个变换交给计算机。新建 transformations_demo.py,如果还没有 NumPy,先安装:

python 复制代码
python3 -m pip install numpy

首先,创建两个基向量的像,再按列组成矩阵:

python 复制代码
import numpy as np

e1 = np.array([1.0, 0.0])
e2 = np.array([0.0, 1.0])
image_e1 = np.array([1.0, -2.0])
image_e2 = np.array([3.0, 0.0])
A = np.column_stack((image_e1, image_e2))
vector = np.array([-1.0, 2.0])
result = A @ vector

print("变换矩阵:")
print(A)
print("输入向量:", vector)
print("输出向量:", result)

输出结果:

python 复制代码
变换矩阵:
[[ 1.  3.]
 [-2.  0.]]
输入向量: [-1.  2.]
输出向量: [5. 2.]

这里 np.column_stack() 把两个一维数组分别作为矩阵的列,接口说明见 NumPy 官方文档。如果直接写 np.array([image_e1, image_e2]),这两个数组会放在行里,得到的就不是当前需要的排列。

用基向量和线性组合,再核对一次

接下来,我们直接检查矩阵作用于标准基向量的结果,再手动按列组合:

python 复制代码
by_columns = vector[0] * A[:, 0] + vector[1] * A[:, 1]

print("第一个基向量的像:", A @ e1)
print("第二个基向量的像:", A @ e2)
print("按列线性组合的结果:", by_columns)
print("两种计算是否一致:", np.allclose(result, by_columns))

输出结果:

python 复制代码
第一个基向量的像: [ 1. -2.]
第二个基向量的像: [3. 0.]
按列线性组合的结果: [5. 2.]
两种计算是否一致: True

A[:, 0] 取第一列,A[:, 1] 取第二列。乘以 会选出第一列,乘以 ​ 会选出第二列,这也从数值上解释了矩阵列的含义。

教材中将这些像按列组成的矩阵称为变换的标准矩阵,相关定理见 The Matrix of a Linear Transformation。

在 NumPy 里,矩阵向量乘法使用 @

代码里还有一个容易混淆的符号。对于当前使用的 ndarray,A @ vector 是矩阵向量乘法;A * vector 则会按照广播规则执行逐元素乘法。

我们继续运行:

python 复制代码
print("使用 @:", A @ vector)
print("使用 *:")
print(A * vector)
print("@ 的结果形状:", (A @ vector).shape)
print("* 的结果形状:", (A * vector).shape)

输出结果:

python 复制代码
使用 @: [5. 2.]
使用 *:
[[-1.  6.]
 [ 2.  0.]]
@ 的结果形状: (2,)
* 的结果形状: (2, 2)

可以看到,* 的结果仍然保留了二维数组结构,并没有把各列相加,因此不是我们要的输出向量。按照 NumPy matmul 官方文档,@ 对应矩阵乘法的语义,当前也可以写成 np.matmul(A, vector)。

先确认数学上要做什么,再确认程序中的运算符是否表达了这件事。 代码能够运行,并不等于已经实现了想要的变换。

看两个基向量,就能写出旋转与剪切矩阵

绕原点,逆时针旋转 90°

接下来,我们练习从几何过程反推出矩阵。先让整个平面绕原点逆时针旋转 。

原来向右的 ​,旋转后指向上方;原来向上的 ​,旋转后指向左方。所以:

按照"第一个像放第一列,第二个像放第二列",直接得到:

这一次,我们没有先记忆旋转矩阵的形式,而是先观察基向量,再把坐标填进对应的列。

水平剪切:高度不变,水平方向随高度偏移

再看一个叫作剪切 的线性变换。让 ​ 保持不变,让 ​ 从 移动到 ,那么矩阵就是:

从公式看,竖直分量 没有改变,水平方向则增加了 。越往上,向右偏移越多;如果 为负,就向左偏移。原点仍然不动。

旋转保持长度和夹角,当前的剪切会改变某些长度与夹角,但它们都保持加法和数乘,所以都是线性变换。

我们使用同一个向量 ,对照两种结果:

python 复制代码
R = np.array([[0.0, -1.0], [1.0, 0.0]])
S = np.array([[1.0, 1.0], [0.0, 1.0]])
sample = np.array([2.0, 3.0])

print("原向量:", sample)
print("逆时针旋转 90°:", R @ sample)
print("水平剪切:", S @ sample)

输出结果:

python 复制代码
原向量: [2. 3.]
逆时针旋转 90°: [-3.  2.]
水平剪切: [5. 3.]

现在大家可以对照图像解释数字:旋转后,原来的高度对应向左的位移,原来的水平位移对应新的高度;剪切后,高度仍然是 ,水平方向从 变成 。

如果两列线性相关,整个平面会被压到哪里?

矩阵的列,不一定还能构成一组基

前面我们一直说矩阵的列是"基向量变换后的像",这里的说法需要保留完整。输入的标准基向量线性无关,并不保证它们的像仍然线性无关。

例如考虑:

第一列是 ,第二列是它的两倍。对任意输入向量:

结果永远落在直线 上。虽然输入遍布整个二维平面,输出却只能填满一条过原点的直线。这些像无法构成二维空间的一组基。

如果矩阵的两列都是零向量,结果就连一条直线也无法填满,只能落在原点。

图中用有限采样点说明压缩效果。真正的输出范围,分别是整个平面、整条直线,以及只有原点的集合。

这正好对应上一篇的张成空间:矩阵所有可能的输出,就是矩阵各列张成的空间。 两列线性无关时可以覆盖二维平面;共线且不全为零时只能覆盖直线;两列全零时只能得到零向量。

不同输入,可能得到相同输出

这样的压缩会带来什么?我们可以检查两个不同的输入:

非零向量 和零向量,都变到了原点。只看输出,就无法唯一恢复原来的输入。后面学习逆矩阵和零空间时,我们还会回到这个现象。

先用代码观察结果:

python 复制代码
C = np.array([[1.0, 2.0], [1.0, 2.0]])
zero_matrix = np.zeros((2, 2))
hidden = np.array([-2.0, 1.0])

print("原输入经过压缩:", C @ vector)
print("非零向量经过压缩:", C @ hidden)
print("零向量经过压缩:", C @ np.zeros(2))
print("零矩阵的输出:", zero_matrix @ vector)
print("C 的秩:", np.linalg.matrix_rank(C))
print("零矩阵的秩:", np.linalg.matrix_rank(zero_matrix))

输出结果:

python 复制代码
原输入经过压缩: [3. 3.]
非零向量经过压缩: [0. 0.]
零向量经过压缩: [0. 0.]
零矩阵的输出: [0. 0.]
C 的秩: 1
零矩阵的秩: 0

这里继续使用上一篇借用过的秩:它记录列向量张成空间的维数。C 的输出只覆盖一维直线,秩为 ;零矩阵只输出零向量,秩为 。对于浮点数组,接口根据奇异值和容差判断数值秩,具体见 NumPy matrix_rank 文档。

把一整个图形,交给同一个矩阵

将多个输入向量,分别放在不同列

到这里,我们已经能够计算单个向量的去向。实际编写图形程序时,往往需要让多个顶点同时变换。

比如一个三角形,三个顶点分别为 、、。把每个顶点的坐标放成一列,就得到:

对它计算 ,就是把同一个变换分别作用到这三列上。这里先把矩阵看成多个列向量的集合,下一篇再详细讨论矩阵乘法与变换复合。

python 复制代码
points = np.array([[0.0, 1.0, 0.0],
                   [0.0, 0.0, 1.0]])
transformed_points = A @ points

print("原顶点,每列一个:")
print(points)
print("变换后的顶点:")
print(transformed_points)
print("输出形状:", transformed_points.shape)

输出结果:

python 复制代码
原顶点,每列一个:
[[0. 1. 0.]
 [0. 0. 1.]]
变换后的顶点:
[[ 0.  1.  3.]
 [ 0. -2.  0.]]
输出形状: (2, 3)

这意味着原点仍是原点,另外两个顶点分别变成 和 。它们恰好就是 的第一列和第二列。

对于线段上的点,线性变换也保持相同的线性组合关系,因此原来的三条边会对应到新图形的三条边。这是顶点计算能够和整个图形的变化对应起来的原因。

当前 points 的形状是 (2, 3),约定每列是一个二维输入。实际使用其他数据文件时,要先确认顶点按行存储还是按列存储,不能只因为数组里有相同的数字,就直接套用同一乘法顺序。

空间发生变换,与换一组坐标描述,要区分开

大家可能会发现,上一篇我们也把两个基向量放进矩阵,然后与坐标相乘。它们的计算形式很相似,但需要看清当前矩阵的含义。

上一篇的 ,是在用新基下的系数,重建同一个几何向量的标准坐标。当前的 ,则是把输入向量实际映射成另一个向量;输入和输出都用固定的标准坐标表示。

所以,"原来的组合系数保持不变",说的是 中的 。它不表示输出的标准坐标仍然是 。本篇的例子中,标准坐标已经从 变成了 。

看到一个矩阵,先读出两根箭头的去向

现在我们再看到一个二维变换矩阵,就可以先读第一列和第二列,画出两个标准基向量的像。然后用输入向量的两个分量作为系数,分别缩放这两根箭头,再相加。

大家可以先做三个练习:

  1. 给定矩阵 ,两个标准基向量分别变到了哪里?输入 又会变到哪里?
  2. 变换 是不是线性变换?能否通过检查原点,直接作出判断?
  3. 对前面的压缩矩阵 ,除了零向量,再找一个被映射到原点的向量。这样的非零输入只有一个吗?

第一题的两个像分别是 和 ,输入的像为 。第二题不是线性变换,因为原点被映射到 。第三题可以取 的任意非零倍数,它们都会变成零向量。

下一篇,我们继续学习矩阵乘法与变换复合:先做一次变换,再做另一次变换,怎样合并成一个矩阵?

参考资料与配套内容

相关推荐
keyipatience1 小时前
TS学习(3)交叉类型与泛型深度解析
1024程序员节
嘶哈哈哈1 小时前
PX4 使用 MTF-01 光流测距一体传感器:接线、参数配置与室内定点
1024程序员节
m0_639906941 小时前
JVM 垃圾回收 GC 全解|判断对象死亡、GC 算法、4 种引用、CMS/G1/ZGC 收集器详解
1024程序员节
茉莉玫瑰花茶1 小时前
线性代数 [ 矩阵乘法与线性变换复合的联系 ]
python·线性代数·math
淡海水2 小时前
07-04-源码-.NET Core GC清扫与压缩
unity·架构·c#·.netcore·gc·1024程序员节
深频率2 小时前
上线一年就撤:微软关掉Outlook的AI邮件助理
人工智能·1024程序员节
橙光成长3 小时前
PDF离线工具箱深度解析:4MB纯前端单文件,40+功能全本地运行,文件绝不上传
前端·pdf·1024程序员节
可爱系程序猿7 小时前
佳能 MF645Cx Windows 11 驱动下载安装:UFR II 队列可打印但扫描入口缺失的排查
程序人生·电脑·1024程序员节
irislinAmelia10 小时前
缺陷检测AI关键点在哪
人工智能·嵌入式硬件·1024程序员节