线性代数 [ 矩阵乘法与线性变换复合的联系 ]

线性代数

两个矩阵相乘,到底把什么乘在了一起?

我们前面已经把矩阵和线性变换联系起来了。一个二维线性变换,只要知道两个标准基向量分别去了哪里,就能够知道整个平面怎样变化。把这两个结果依次放进矩阵的两列,这个变换就被记录下来了。

接下来,我们再往前走一步:如果先做一个变换,再做另一个变换,应该怎样描述整个过程?

比如,先让整个平面绕原点逆时针旋转 ,然后再做一次水平剪切。我们可以分两步计算,也可以直接给出一个矩阵,让它一次得到最后的结果。

矩阵乘法所记录的,就是这样一个连续执行的过程。 把这一点想清楚,为什么要"行乘列"、为什么顺序不能随意交换、为什么又满足结合律,就可以连起来理解。

全文沿用列向量约定:矩阵写在向量左边;输入、输出都用固定的标准坐标系表示。后面的三维旋转也在这个约定下讨论。

先旋转,再剪切:把两个变换接起来

先把每一步单独看清楚

我们继续使用上一篇出现过的两个矩阵:

表示绕原点逆时针旋转 ,把 变成 ; 表示水平剪切,把 变成 。

这里的剪切可以这样看:高度 不变,水平方向的移动量等于 。所以 轴上的点不动,位于 的点向右移动 ,位于 的点向左移动 。

现在给一个输入向量:

先旋转:

再对这个结果做剪切:

大家看下面这张图。蓝色箭头追踪的是同一个输入向量,绿色、黄色箭头追踪的是原来的两个标准基向量。每一步都接着上一步的结果继续变化。

图中蓝色区域是同一个矩形在各阶段的位置。网格只显示有限的观察范围,变换规则仍然作用于整个平面。

为什么整体效果仍然能写成一个矩阵?

先后执行两个线性变换,得到的复合变换仍然是线性的。我们把两个变换分别记为 ,对任意向量和实数 ,有:

这个式子只是连续使用了两次"保持线性组合"的规则。既然整体仍然是线性变换,我们就可以继续追踪基向量,把整个过程写成一个矩阵。

这个矩阵记为 ,满足:

等号要求对任意输入向量 成立。它说的是,两步变换可以合并成一个变换,而不只是刚才那个输入恰好算出相同的结果。教材对这种对应关系有正式说明,见 Interactive Linear Algebra:Composition and Matrix Multiplication。

为什么先做的变换,写在右边?

这里特别容易把顺序看反。我们说"先旋转,再剪切",写出来却是 ,剪切矩阵 放在左边。

原因可以直接从函数记号读出来: 要先算括号里面的 ,再把结果交给 ​。矩阵写法沿用了同样的顺序。

因此,在当前列向量约定下:

离输入向量最近的矩阵先执行。 看到 ,我们就把它读成:先做 所表示的变换,再做 所表示的变换。

用代码同时走两条路线

下面使用 NumPy。首次运行需要安装依赖:python3 -m pip install numpy。本篇代码块按顺序执行;一维数组用于保存向量,@ 表示矩阵乘法,规则可对照 NumPy 官方文档:matmul。

python 复制代码
import numpy as np

R = np.array([[0, -1], [1, 0]])
S = np.array([[1, 1], [0, 1]])
v = np.array([2, 1])

rotated = R @ v
step_by_step = S @ rotated
C = S @ R
print("旋转后:", rotated)
print("再剪切:", step_by_step)
print("复合矩阵:\n", C, sep="")
print("直接计算:", C @ v)
print("两种方式一致:", np.array_equal(step_by_step, C @ v))

输出结果:

python 复制代码
旋转后: [-1  2]
再剪切: [1 2]
复合矩阵:
[[ 1 -1]
 [ 1  0]]
直接计算: [1 2]
两种方式一致: True

逐步计算和直接计算都得到了 。我们还得到一个复合矩阵:

现在问题就变成了:这个矩阵中的四个数字,能不能从基向量的去向推出来?

矩阵乘法,为什么要这样计算?

先追踪第一根基向量

这里换另一组矩阵,把计算过程完整拆开:

我们要计算 ​,也就是先做 ​,再做 。

上一篇已经讲过:矩阵的第一列,是第一根标准基向量的像。 因此,复合矩阵的第一列,就是 ​ 经历两次变换以后到达的位置。

第一步,读取 ​ 的第一列:

第二步,把这个中间结果交给 ​:

所以,乘积的第一列是 T。

第二根基向量,按照相同的方式继续

读取 ​ 的第二列,可以知道 ​ 先到达 。再做 ​:

把两个最终结果按列放好,就得到:

绿色箭头最后到达 ,黄色箭头最后到达 。这两根箭头提供的信息,恰好就是复合矩阵的两列。

所以,我们现在可以把计算规则说得很具体:用左边的矩阵,分别乘右边矩阵的每一列,再把结果依次放进乘积的对应列。

代码可以直接照着这个过程写:

python 复制代码
M1 = np.array([[1, -2], [1, 0]])
M2 = np.array([[0, 2], [1, 0]])
first = M2 @ M1[:, 0]
second = M2 @ M1[:, 1]
by_columns = np.column_stack((first, second))
print("第一列:", first)
print("第二列:", second)
print("按列组成:\n", by_columns, sep="")
print("与矩阵乘法一致:", np.array_equal(by_columns, M2 @ M1))

输出结果:

python 复制代码
第一列: [2 1]
第二列: [ 0 -2]
按列组成:
[[ 2  0]
 [ 1 -2]]
与矩阵乘法一致: True

这里 M1[:, 0] 取出第一列,M1[:, 1] 取出第二列;np.column_stack 把它们的最终去向重新按列排列。每一行代码,都能对应到图中的一步。

把数字换成字母,就得到一般公式

设:

先做 ,第一根标准基到达 。再做 ,就得到:

第二根标准基经过相同的过程,最终到达 。因此:

现在再看"左矩阵的第 行,乘右矩阵的第 列",就知道它在计算什么了:它在读取第 根标准基经过整个复合变换后,第 个坐标是多少。

一般情况下,若 有 列,记乘积为 ,元素公式就是:

这里的 遍历中间向量的所有分量。行乘列是展开后的计算方式,追踪基向量则说明了这些分量为什么要这样相乘、相加。

为什么矩阵乘法一般不能交换顺序?

把剪切和旋转调换一下

我们回到最开始的 。刚才做的是先旋转,再剪切,复合矩阵为 。

现在先剪切,再旋转。输入仍然是 ,先经过 到达 ,再经过 到达 。

而原来的顺序,终点是 。同一个输入、同样两种变换,执行顺序一变,输出就变了。

大家可以重点看两幅图中的蓝色箭头。剪切的方向由固定坐标系确定,它与旋转接在一起时,先后关系会影响最终位置。我们把两个乘积都写出来:

矩阵不同,对应的线性变换也不同。用代码验证:

python 复制代码
print("先旋转后剪切 SR:\n", S @ R, sep="")
print("先剪切后旋转 RS:\n", R @ S, sep="")
print("SR 作用于 v:", (S @ R) @ v)
print("RS 作用于 v:", (R @ S) @ v)

输出结果:

python 复制代码
先旋转后剪切 SR:
[[ 1 -1]
 [ 1  0]]
先剪切后旋转 RS:
[[ 0 -1]
 [ 1  1]]
SR 作用于 v: [1 2]
RS 作用于 v: [-1  3]
"一般不交换",不等于"永远不相等"

这里要把结论说准确:矩阵乘法不满足普遍的交换律。 我们不能对任意两个矩阵都写 。

但确实存在可以交换的矩阵。例如,单位矩阵 表示什么都不改变,同一大小的方阵总有 。某些其他变换组合,也可能得到相同的效果。

还有一点:不同的变换,可能把某些特定向量送到相同位置。比如刚才的两个顺序都把零向量送到零向量。因此,仅仅检查一个输入得到了相同输出,不能证明两个矩阵相同;要证明它们不同,找到一个输出不同的输入就足够了。

为什么不能交换,却可以改变括号?

结合律保持了原来的执行顺序

接下来考虑三个线性变换,矩阵分别记为 。让它们按 、、 的顺序执行:

我们可以先把 合成一个矩阵,然后接上 ,写成 ;也可以先把 合成一个矩阵,再接在 后面,写成 。

这两种分组,对任意输入执行的完整过程都是"先 ,再 ,最后 "。所以它们对每个标准基向量的输出相同,矩阵的每一列也就相同。这就得到了:

这个几何解释对应的,就是矩阵乘法的结合律 。原课程对顺序与分组的讨论,可以对照 3Blue1Brown:Matrix multiplication as composition 阅读。

这里重点注意:改括号,是改变预先合并哪几步;交换矩阵,是改变真正执行的先后顺序。 两件事要分开。

再加入一个水平方向的拉伸

设 把横坐标扩大到原来的两倍,纵坐标保持不变:

我们先做 ,再做 ,最后做 。两种分组应该给出同一个复合矩阵:

接着运行:

python 复制代码
D = np.array([[2, 0], [0, 1]])
left = (S @ R) @ D
right = S @ (R @ D)
print("共同的复合矩阵:\n", left, sep="")
print("两种分组一致:", np.array_equal(left, right))
print("先 D,再 R,最后 S:", S @ (R @ (D @ v)))

输出结果:

python 复制代码
共同的复合矩阵:
[[ 2 -1]
 [ 2  0]]
两种分组一致: True
先 D,再 R,最后 S: [3 4]

沿着向量也能核对: 先到 ,再到 ,最后到 。

上面的代码是在检查具体例子,一般性的理由仍然是前面的复合过程。这里使用小整数和精确比较;换成浮点数据时,不同分组可能因为舍入产生微小差异,通常需要使用带适当容差的 np.allclose 判断数值是否接近。

推广到三维,改变的是列数,思路仍然一样

三根标准基,记录三列

二维的标准基有两根,三维的标准基则有三根:

它们也常被记为 。对一个从 到 的线性变换,只要追踪这三根基向量,就能把结果依次写进一个 矩阵:

上式中的每一个 都是一整列,包含三个坐标。因此,矩阵一共有九个数。对任意输入:

这里仍然是在按输入坐标缩放各列,然后相加。三维画面更复杂,计算背后的线性组合关系并没有改变。对应课程见 3Blue1Brown:Three-dimensional linear transformations。

绕 y 轴旋转的矩阵,怎样从图中读出来?

我们采用右手坐标系,让向量绕固定的 轴,按右手定则旋转正 。这里的正方向可以理解为:右手拇指指向 ,其余手指弯曲的方向就是旋转方向。

按照这个约定,三根基向量分别变成:

其中, 就在旋转轴上,所以保持不动。把三个结果按列排列:

灰线保留了固定坐标轴,彩色箭头则追踪原来的基向量。这里观察的是向量在固定坐标系中的旋转,后面的矩阵也都按这个方式解释。

任意输入的计算结果,可以直接从三列读出来:

比如 会到达 。用代码检查三列和这个输出:

python 复制代码
Ry = np.array([[0, 0, 1], [0, 1, 0], [-1, 0, 0]])
v3 = np.array([1, 2, 3])
print("e1 的像:", Ry[:, 0])
print("e2 的像:", Ry[:, 1])
print("e3 的像:", Ry[:, 2])
print("Ry 作用于 v3:", Ry @ v3)

输出结果:

python 复制代码
e1 的像: [ 0  0 -1]
e2 的像: [0 1 0]
e3 的像: [1 0 0]
Ry 作用于 v3: [ 3  2 -1]
三维的矩阵乘法,也是在接着做变换

现在再接上一个绕固定 轴、按右手定则旋转正 的变换:

它让 变成 。从 一侧看向原点,在 平面中就是逆时针旋转 。

先绕 轴,再绕 轴,复合矩阵仍然从右往左写:

我们还是可以只看基向量。例如,​ 先经 ​ 到达 ​,再经 ​ 到达 。所以乘积的第三列就是 。

这里两次旋转都围绕同一个固定坐标系的轴进行。图中颜色始终跟随原来的那根基向量,所以第三幅图的绿色、黄色、蓝色箭头,分别对应乘积的第一、第二、第三列。

沿着输入向量看,整个过程是:

继续运行:

python 复制代码
Rz = np.array([[0, -1, 0], [1, 0, 0], [0, 0, 1]])
C3 = Rz @ Ry
print("先 y 后 z 的复合矩阵:\n", C3, sep="")
print("逐步计算:", Rz @ (Ry @ v3))
print("直接计算:", C3 @ v3)
print("交换顺序:", (Ry @ Rz) @ v3)

输出结果:

python 复制代码
先 y 后 z 的复合矩阵:
[[ 0 -1  0]
 [ 0  0  1]
 [-1  0  0]]
逐步计算: [-2  3 -1]
直接计算: [-2  3 -1]
交换顺序: [3 1 2]

交换两次旋转,结果又变了。二维和三维在这里遵循相同的规则:每一列追踪一个基向量,右侧变换先执行,左侧变换接着处理它的输出。

两个矩阵,是不是总能放在一起相乘?

前面的矩阵都是方阵,输入、输出维数相同,所以很容易忽略维数问题。现在把中间过程写出来:

接受 个分量,输出 个分量,所以它是 矩阵。 必须能够接受这 个分量,才能接着往下算,因此它是 矩阵。

整个过程从 到 ,乘积的大小自然就是:

这就是"左边列数等于右边行数"的原因:前一个变换输出的分量数,要与后一个变换接收的分量数匹配。

例如,一个 矩阵把二维向量送入三维空间,一个 矩阵再把三维向量送回二维空间。按这个顺序复合,会得到 矩阵;反过来复合,则得到 矩阵。它们连输入空间都不同,不能直接写成两个相等的矩阵。有些其他大小的矩阵,调换顺序后甚至无法相乘。

实际写代码时,为什么要先求出复合矩阵?

如果只有一个向量,分两步计算就很直观。但如果同一组变换要应用到很多个顶点,我们可以先求出复合矩阵,然后反复使用。

以前面的矩形为例,把四个顶点分别放进四列:

每一列都是一个位置向量,依次对应 、、、。同一个矩阵左乘 ,就会分别作用于这些列。

我们已经算出 ,因此:

python 复制代码
points = np.array([[0, 2, 2, 0], [0, 0, 1, 1]])
result = C @ points
print("每列一个变换后的顶点:\n", result, sep="")
print("与逐步变换一致:", np.array_equal(result, S @ (R @ points)))

输出结果:

python 复制代码
每列一个变换后的顶点:
[[ 0  2  1 -1]
 [ 0  2  2  0]]
与逐步变换一致: True

这里的第三列就是 ,正好对应第一张过程图里追踪的蓝色箭头。按列连接四个输出点,就能得到最后的平行四边形。

对于这组固定的二维变换,预先保存 ,之后每一批顶点只需要应用这个复合矩阵。需要保留中间阶段时,仍然可以分步计算。具体采用哪一种方式,要看是否复用相同的变换,以及是否需要观察每一步。

还有一个代码细节要一直记住:对这里的 NumPy 数组,S @ R 才表示变换的复合;S * R 表示对应元素相乘。两者计算的是不同的东西。

到这里,可以自己检查哪几件事?

先不要急着重新背公式。大家可以试着离开代码,回答下面两个问题。

第一个:不展开完整乘积,怎样求 ​ 的第二列? 只追踪 :它先到达 ,再到达 ,这就是第二列。

第二个: 与 为什么相同,而 不一定与它们相同? 前两种分组都执行"先拉伸、再旋转、最后剪切"; 则执行"先拉伸、再剪切、最后旋转",真正的执行顺序发生了变化。

如果能把这两个问题讲清楚,再看到矩阵乘法,就能够同时读出它的公式和它所描述的空间过程。

下一篇,我们继续观察空间怎样变化:一个线性变换,会把面积或体积扩大多少倍?有没有把空间压扁?又有没有改变方向?这些问题会把我们带到行列式。

相关推荐
garmin Chen1 小时前
Rag引入
python·agent·rag
小oo呆5 小时前
【学习心得】Python多线程
python
happylifetree9 小时前
Python70:核心语法-函数进阶-案例2
python
王中阳Go10 小时前
两个入口答案不一致先别调模型:6 行入口口径对账表(附脚本)
人工智能·后端·python·大语言模型·1024程序员节
BK贝壳10 小时前
bool-hybrid-array 落盘我只认 .bha:用 Create_BHA / Ask_BHA 做布尔数组序列化
数据结构·python·numpy·1024程序员节
Sakura_j_h_10 小时前
Python 在定时任务里输出 emoji,UnicodeEncodeError 从哪来
开发语言·字符编码·windows·python·定时任务
supabc12312 小时前
欧盟电子发票(XRechnung/Peppol/ZUGFeRD)踩坑记:用 Python 不装 Java 跑官方校验规则
xml·python·开源·电子发票·1024程序员节
ZhengEnCi12 小时前
AI Agent工具调用防护层怎么写-一份能直接跑的Python四层拦截实现
python·llm·agent
ZhengEnCi12 小时前
AI Agent工具误调用怎么解决-从参数校验到高危二次确认的四层工程防线
python·llm·agent