线性代数
四个数字,为什么能够描述整个平面的变化?
现在给大家一个矩阵:
这四个数字到底在描述什么?如果我们只记住“第一行乘输入向量,得到第一个分量”,当然也能算出结果。但这样一来,矩阵为什么这么乘、结果对应怎样的空间变化,就很容易变成需要另外记忆的内容。
这一篇,我们换个观察顺序:先看空间怎样变化,再看两个基向量去了哪里,最后把这些信息写成矩阵。理解了这个过程,矩阵向量乘法就能和上一篇的线性组合联系起来。
本篇重点讨论的概念,就是线性变换与它的矩阵表示。
文中的代码、配图和计算说明为配套补充。当前主要讨论实数二维向量之间的变换,输入和输出都使用固定的标准坐标系。
变换,就是把一个向量对应到另一个向量
先把它看成一个函数
我们先不考虑矩阵。一个变换,本质上就是一个函数:给它一个输入,它按照确定的规则返回一个输出。
在当前讨论中,输入和输出都是向量。我们把变换记为,写成:
这里的表示所有实数二维向量组成的空间。
表示向量
经过这个变换以后得到的结果。
比如一个向量原来指向,经过变换以后指向
,我们可以把它想成:这个向量的终点,从原来的位置移动到了新的位置。
不过,一个输入向量的去向,并不能说明整个变换。我们还要知道,同一套规则作用到其他向量时,会发生什么。
为什么课程里要画一整张网格?
如果把所有向量都画成从原点出发的箭头,画面很快就会挤满。所以这里继续使用上一篇的方法:用箭头的终点来代表向量。
接下来,选取一组水平、竖直的网格线,让上面的每一个点都按照同一规则移动。这样就能够观察到,整个空间是在旋转、拉伸、剪切,还是发生了更复杂的弯曲。
后面的配图会保留淡色的原始网格,再用彩色网格表示变换后的结果。背景坐标轴用于读取位置,彩色网格则记录原来那些点去了哪里。网格只画出有限范围,真正的变换规则作用于整个平面。
什么样的变换,才是线性变换?
先建立图像,再补上正式条件
从几何上观察,线性变换保持原点不动;一条直线经过变换以后,不会弯成曲线。同一族平行、等距的网格线,变换后仍呈现规则的平行、等距结构,发生压缩退化时则可能重合。
这里的“等距”不是指长度保持不变,而是说原来等间隔排列的点或网格,经过变换后,仍按照一致的步长排列。线性变换可以改变长度,也可以改变夹角。
这些图像帮助我们识别现象。真正用来计算和推导的定义,是下面两条:
它们要求对所有输入向量和所有实数
都成立。第一条表示保持向量加法,第二条表示保持数乘。正式定义可以对照教材 Interactive Linear Algebra:Linear Transformations 阅读。
把两条规则放在一起,就得到本篇最重要的关系:
先线性组合,再做变换;或者先分别变换,再使用原来的系数组合,结果相同。后面为什么只需要追踪基向量,就是由这条关系决定的。
原点不动,是必要条件,还不是全部条件
令数乘系数为零,就能得到。因此,线性变换一定把零向量映射到零向量。
如果把整个平面向右平移、向上平移
,原点就到了
。这种变换虽然让直线保持平直,但它不满足当前向量空间中的线性变换定义。
这个平移可以写成:
还有一种情况,原点没有移动,变换却仍然不线性。比如:
原点确实还在原点,但水平直线会弯曲。取,就能直接检查出:
,而
,数乘关系不成立。
图中左边的剪切是线性变换;中间的平移、右边的弯曲都不是。检查时,既要观察原点,也要观察整条直线和间隔,不能只挑几个看起来没有问题的位置。
还有一个边界需要补充:线性变换允许把直线压缩成一个点。比如把所有二维向量都变成零向量,完全符合加法和数乘规则。因此,“直线保持平直”的图像,也需要包含这种退化情况。
只记录两个基向量,为什么就够了?
任意向量,先写成标准基的线性组合
前面在《线性组合·张成的空间与基》中,我们已经知道:
它们构成二维空间的标准基。任意向量都可以写成:
现在对它做线性变换,按照前面的规则:
大家重点看这里:两个系数没有改变,改变的是它们所缩放的向量。原来缩放
,现在缩放
。
因此,只要知道两个标准基向量的像,也就是它们变换后的结果,就能够算出任意输入向量的像。
这里有一个前提:我们已经知道是线性变换。如果只是随意给出两个输入点的输出,而没有线性条件,就不能推断其他点的去向。
完整算一遍
假设两个基向量分别变到了:
我们想知道经过变换后会到哪里。先把输入写成
,再保持这组组合系数:
图中绿色、黄色箭头分别对应两个标准基向量及其变换结果,蓝色箭头对应输入向量与输出向量。虚线标出了使用相同系数、先缩放再相加的路径。
我们并不需要先观察所有网格点的运动,再去读取蓝色箭头的终点。知道两根基向量的去向,按照原系数组合,就已经确定了答案。
矩阵的两列,就是两个基向量变换后的坐标
把前面的信息,按列放在一起
我们把放在第一列,把
放在第二列,就得到:
这就是开头的矩阵。第一列告诉我们变到了
;第二列告诉我们
变到了
。
注意这里读取的是列。第一行表示两个输入分量怎样共同贡献到输出的第一个分量,它不是第一个基向量的像。
更一般地,如果:
那么矩阵向量乘法就是:
按行计算输出分量,按列理解基向量的去向。两种看法得到的是同一个乘法规则,不需要把它们分开记成两套知识。
对于当前例子,任意输入都会变成
。四个数字之所以足够,是因为二维输入只有两个标准基向量,而每个像又需要两个坐标描述。
使用 Python 构造矩阵,再计算结果
接下来,我们把这个变换交给计算机。新建transformations_demo.py,如果还没有 NumPy,先安装:
python3 -m pip install numpy首先,创建两个基向量的像,再按列组成矩阵:
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)输出结果:
变换矩阵: [[ 1. 3.] [-2. 0.]] 输入向量: [-1. 2.] 输出向量: [5. 2.]这里np.column_stack()把两个一维数组分别作为矩阵的列,接口说明见 NumPy 官方文档。如果直接写np.array([image_e1, image_e2]),这两个数组会放在行里,得到的就不是当前需要的排列。
用基向量和线性组合,再核对一次
接下来,我们直接检查矩阵作用于标准基向量的结果,再手动按列组合:
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))输出结果:
第一个基向量的像: [ 1. -2.] 第二个基向量的像: [3. 0.] 按列线性组合的结果: [5. 2.] 两种计算是否一致: TrueA[:, 0]取第一列,A[:, 1]取第二列。乘以会选出第一列,乘以
会选出第二列,这也从数值上解释了矩阵列的含义。
教材中将这些像按列组成的矩阵称为变换的标准矩阵,相关定理见 The Matrix of a Linear Transformation。
在 NumPy 里,矩阵向量乘法使用 @
代码里还有一个容易混淆的符号。对于当前使用的ndarray,A @ vector是矩阵向量乘法;A * vector则会按照广播规则执行逐元素乘法。
我们继续运行:
print("使用 @:", A @ vector) print("使用 *:") print(A * vector) print("@ 的结果形状:", (A @ vector).shape) print("* 的结果形状:", (A * vector).shape)输出结果:
使用 @: [5. 2.] 使用 *: [[-1. 6.] [ 2. 0.]] @ 的结果形状: (2,) * 的结果形状: (2, 2)可以看到,*的结果仍然保留了二维数组结构,并没有把各列相加,因此不是我们要的输出向量。按照 NumPy matmul 官方文档,@对应矩阵乘法的语义,当前也可以写成np.matmul(A, vector)。
先确认数学上要做什么,再确认程序中的运算符是否表达了这件事。代码能够运行,并不等于已经实现了想要的变换。
看两个基向量,就能写出旋转与剪切矩阵
绕原点,逆时针旋转 90°
接下来,我们练习从几何过程反推出矩阵。先让整个平面绕原点逆时针旋转。
原来向右的,旋转后指向上方;原来向上的
,旋转后指向左方。所以:
按照“第一个像放第一列,第二个像放第二列”,直接得到:
这一次,我们没有先记忆旋转矩阵的形式,而是先观察基向量,再把坐标填进对应的列。
水平剪切:高度不变,水平方向随高度偏移
再看一个叫作剪切的线性变换。让 保持不变,让
从
移动到
,那么矩阵就是:
从公式看,竖直分量没有改变,水平方向则增加了
。越往上,向右偏移越多;如果
为负,就向左偏移。原点仍然不动。
旋转保持长度和夹角,当前的剪切会改变某些长度与夹角,但它们都保持加法和数乘,所以都是线性变换。
我们使用同一个向量,对照两种结果:
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)输出结果:
原向量: [2. 3.] 逆时针旋转 90°: [-3. 2.] 水平剪切: [5. 3.]现在大家可以对照图像解释数字:旋转后,原来的高度对应向左的位移,原来的水平位移对应新的高度;剪切后,高度仍然是,水平方向从
变成
。
如果两列线性相关,整个平面会被压到哪里?
矩阵的列,不一定还能构成一组基
前面我们一直说矩阵的列是“基向量变换后的像”,这里的说法需要保留完整。输入的标准基向量线性无关,并不保证它们的像仍然线性无关。
例如考虑:
第一列是,第二列是它的两倍。对任意输入向量:
结果永远落在直线上。虽然输入遍布整个二维平面,输出却只能填满一条过原点的直线。这些像无法构成二维空间的一组基。
如果矩阵的两列都是零向量,结果就连一条直线也无法填满,只能落在原点。
图中用有限采样点说明压缩效果。真正的输出范围,分别是整个平面、整条直线,以及只有原点的集合。
这正好对应上一篇的张成空间:矩阵所有可能的输出,就是矩阵各列张成的空间。两列线性无关时可以覆盖二维平面;共线且不全为零时只能覆盖直线;两列全零时只能得到零向量。
不同输入,可能得到相同输出
这样的压缩会带来什么?我们可以检查两个不同的输入:
非零向量和零向量,都变到了原点。只看输出,就无法唯一恢复原来的输入。后面学习逆矩阵和零空间时,我们还会回到这个现象。
先用代码观察结果:
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))输出结果:
原输入经过压缩: [3. 3.] 非零向量经过压缩: [0. 0.] 零向量经过压缩: [0. 0.] 零矩阵的输出: [0. 0.] C 的秩: 1 零矩阵的秩: 0这里继续使用上一篇借用过的秩:它记录列向量张成空间的维数。C的输出只覆盖一维直线,秩为;零矩阵只输出零向量,秩为
。对于浮点数组,接口根据奇异值和容差判断数值秩,具体见 NumPy matrix_rank 文档。
把一整个图形,交给同一个矩阵
将多个输入向量,分别放在不同列
到这里,我们已经能够计算单个向量的去向。实际编写图形程序时,往往需要让多个顶点同时变换。
比如一个三角形,三个顶点分别为、
、
。把每个顶点的坐标放成一列,就得到:
对它计算,就是把同一个变换分别作用到这三列上。这里先把矩阵看成多个列向量的集合,下一篇再详细讨论矩阵乘法与变换复合。
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)输出结果:
原顶点,每列一个: [[0. 1. 0.] [0. 0. 1.]] 变换后的顶点: [[ 0. 1. 3.] [ 0. -2. 0.]] 输出形状: (2, 3)这意味着原点仍是原点,另外两个顶点分别变成和
。它们恰好就是
的第一列和第二列。
对于线段上的点,线性变换也保持相同的线性组合关系,因此原来的三条边会对应到新图形的三条边。这是顶点计算能够和整个图形的变化对应起来的原因。
当前points的形状是(2, 3),约定每列是一个二维输入。实际使用其他数据文件时,要先确认顶点按行存储还是按列存储,不能只因为数组里有相同的数字,就直接套用同一乘法顺序。
空间发生变换,与换一组坐标描述,要区分开
大家可能会发现,上一篇我们也把两个基向量放进矩阵,然后与坐标相乘。它们的计算形式很相似,但需要看清当前矩阵的含义。
上一篇的,是在用新基下的系数,重建同一个几何向量的标准坐标。当前的
,则是把输入向量实际映射成另一个向量;输入和输出都用固定的标准坐标表示。
所以,“原来的组合系数保持不变”,说的是中的
。它不表示输出的标准坐标仍然是
。本篇的例子中,标准坐标已经从
变成了
。
看到一个矩阵,先读出两根箭头的去向
现在我们再看到一个二维变换矩阵,就可以先读第一列和第二列,画出两个标准基向量的像。然后用输入向量的两个分量作为系数,分别缩放这两根箭头,再相加。
大家可以先做三个练习:
- 给定矩阵
,两个标准基向量分别变到了哪里?输入
又会变到哪里?
- 变换
是不是线性变换?能否通过检查原点,直接作出判断?
- 对前面的压缩矩阵
,除了零向量,再找一个被映射到原点的向量。这样的非零输入只有一个吗?
第一题的两个像分别是和
,输入的像为
。第二题不是线性变换,因为原点被映射到
。第三题可以取
的任意非零倍数,它们都会变成零向量。
下一篇,我们继续学习矩阵乘法与变换复合:先做一次变换,再做另一次变换,怎样合并成一个矩阵?
参考资料与配套内容
- 上一篇:线性代数 [ 线性组合·张成的空间与基 ]。
- 向量基础:线性代数 [ 向量究竟是什么 ]。
- 正式定义与矩阵表示:Interactive Linear Algebra:Linear Transformations。
- 代码接口:NumPy matmul、column_stack、matrix_rank。