Skip to content

第 7 章 矩阵乘法

学习目标

  • 从线性组合推导「矩阵 × 向量」的含义
  • 推导矩阵 × 矩阵的规则,掌握行×列(点积)与列向量线性组合两种视角
  • 掌握矩阵乘法的尺寸条件
  • 验证结合律、分配律,并理解乘法不满足交换律
  • 理解矩阵乘法 = 线性变换的复合
  • 会用 NumPy 与 PyTorch 做矩阵乘法

7.1 矩阵 × 向量:求一组线性组合

第 5 章我们算了 ,其中 。把两个向量作为列放进一个矩阵:

再把系数 写成向量 。定义

矩阵 × 向量,就是把矩阵的列按向量的分量做线性组合。 一般地:

这就是第 5 章「线性组合」的矩阵写法:求 ,等于用矩阵 乘向量

这个定义要求:矩阵的列数必须等于向量的行数(分量个数)。,,结果

python
import numpy as np

A = np.array([[1.0, 3.0],
              [2.0, 1.0]])
x = np.array([2.0, 3.0])
print("A @ x =", A @ x)

# 等价写法:按定义做线性组合
v1 = A[:, 0]
v2 = A[:, 1]
print("x1*v1 + x2*v2 =", x[0] * v1 + x[1] * v2)

运行输出:

text
A @ x = [11.  7.]
x1*v1 + x2*v2 = [11.  7.]

7.2 矩阵 × 矩阵:对每一列分别乘

现在要算 。思路:把 看成一列一列的向量组, 的每一列就是 的那一列:

所以尺寸条件是: 的列数必须等于 的行数 时,。口诀:里面两个 相等,外面的 是结果的形状。

把「 的第 列」按 7.1 展开,再看第 个分量,会发现另一个视角:

也就是: 的第 行第 列,等于 逐项相乘再相加——行 × 列的点积

两个视角各有用途:列视角解释「乘法在做什么」(线性组合),行×列视角用来手算。

7.3 手算一个 2×2 例子

用行×列:

python
import numpy as np

A = np.array([[1.0, 2.0],
              [3.0, 4.0]])
B = np.array([[5.0, 6.0],
              [7.0, 8.0]])
print("A @ B =\n", A @ B)
print("B @ A =\n", B @ A)
print("AB == BA?", np.allclose(A @ B, B @ A))

运行输出:

text
A @ B =
 [[19. 22.]
 [43. 50.]]
B @ A =
 [[23. 34.]
 [31. 46.]]
AB == BA? False

矩阵乘法不满足交换律:。这是与数的乘法最大的区别。所以写代码、写公式时,永远要分清「」与「」。

验证单位矩阵、结合律、分配律:

python
import numpy as np

A = np.array([[1.0, 2.0],
              [3.0, 4.0]])
B = np.array([[0.0, 1.0],
              [1.0, 0.0]])
C = np.array([[1.0, 1.0],
              [0.0, 1.0]])
I = np.eye(2)

print("AI = A?", np.allclose(A @ I, A))
print("IA = A?", np.allclose(I @ A, A))
print("结合律:", np.allclose((A @ B) @ C, A @ (B @ C)))
print("分配律:", np.allclose(A @ (B + C), A @ B + A @ C))

运行输出:

text
AI = A? True
IA = A? True
结合律: True
分配律: True

7.4 矩阵乘法 = 变换的复合

把矩阵看成一个操作:输入一个向量,输出一个新向量。比如

对向量 :

这是把向量逆时针旋转 90°。矩阵就是「几何变换的代数表达」。

现在连续做两个变换:先乘 得到 ,再乘 得到 。结合律保证:

所以 这个矩阵,本身就是「先做 、再做 」的复合变换。矩阵乘法把一个变换序列压缩成一次乘法——神经网络里一层一层的线性层,就是这么一层层「复合」起来的。

矩阵变换

python
import matplotlib.pyplot as plt
from matplotlib import font_manager
for f in font_manager.findSystemFonts():
    if any(k in f for k in ("NotoSansCJK", "NotoSansSC", "wqy", "SimHei", "msyh", "PingFang")):
        font_manager.fontManager.addfont(f)
plt.rcParams["font.sans-serif"] = ["Noto Sans CJK SC", "WenQuanYi Zen Hei", "SimHei", "Microsoft YaHei", "PingFang SC"]
plt.rcParams["axes.unicode_minus"] = False

import numpy as np

square = np.array([[0.0, 0.0], [1.0, 0.0], [1.0, 1.0], [0.0, 1.0], [0.0, 0.0]])

def draw(ax, pts, color, label):
    ax.plot(pts[:, 0], pts[:, 1], color=color, linewidth=2, label=label)
    ax.fill(pts[:, 0], pts[:, 1], color=color, alpha=0.15)

fig, axes = plt.subplots(1, 2, figsize=(12, 5.5))

# 左图:剪切变换 M
ax = axes[0]
M = np.array([[1.5, 0.5],
              [0.0, 1.0]])
draw(ax, square, "tab:blue", "单位正方形")
draw(ax, square @ M.T, "tab:red", "M 变换后")
for p in square[:-1]:
    q = M @ p
    ax.annotate("", q, xytext=p,
                arrowprops=dict(arrowstyle="->", color="gray", lw=0.8))
ax.axhline(0, color="black", linewidth=1)
ax.axvline(0, color="black", linewidth=1)
ax.set_xlim(-0.5, 2.5)
ax.set_ylim(-0.5, 2.0)
ax.set_aspect("equal")
ax.grid(True, linestyle=":", alpha=0.5)
ax.set_title("一次变换:M 把正方形变成平行四边形")
ax.legend(fontsize=9)

# 右图:复合变换 先 S 拉伸,再 R 旋转
ax = axes[1]
S = np.array([[2.0, 0.0],
              [0.0, 1.0]])
R = np.array([[0.0, -1.0],
              [1.0, 0.0]])
draw(ax, square, "tab:blue", "单位正方形")
draw(ax, square @ S.T, "tab:green", "S 拉伸后")
draw(ax, square @ (R @ S).T, "tab:red", "R(S(·)) 复合后")
ax.axhline(0, color="black", linewidth=1)
ax.axvline(0, color="black", linewidth=1)
ax.set_xlim(-2.5, 2.5)
ax.set_ylim(-2.5, 2.5)
ax.set_aspect("equal")
ax.grid(True, linestyle=":", alpha=0.5)
ax.set_title("复合变换:先 S 再 R,等价于矩阵 RS")
ax.legend(fontsize=9)

fig.savefig("book/public/figs/fig07-transform.png", dpi=120, bbox_inches="tight")
plt.close(fig)
python
import numpy as np

# 逆时针旋转 90° 与水平拉伸的复合
R = np.array([[0.0, -1.0],
              [1.0, 0.0]])
S = np.array([[2.0, 0.0],
              [0.0, 1.0]])

x = np.array([1.0, 2.0])
print("先拉伸后旋转:", R @ (S @ x))
print("复合矩阵一步完成:", (R @ S) @ x)
print("两者相等?", np.allclose(R @ (S @ x), (R @ S) @ x))

运行输出:

text
先拉伸后旋转: [-2.  2.]
复合矩阵一步完成: [-2.  2.]
两者相等? True

7.5 尺寸条件与 PyTorch

尺寸不对会直接报错:

python
# 预期错误:ValueError
import numpy as np

A = np.ones((2, 3))
B = np.ones((2, 2))
print(A @ B)

报错要点:第一个矩阵的列数(3)与第二个矩阵的行数(2)不相等,无法相乘。

PyTorch 的矩阵乘法:

python
import torch

A = torch.tensor([[1.0, 2.0],
                  [3.0, 4.0]])
B = torch.tensor([[5.0, 6.0],
                  [7.0, 8.0]])
print("torch A @ B =\n", A @ B)
print("torch.matmul 同样可用:\n", torch.matmul(A, B))

运行输出:

text
torch A @ B =
 tensor([[19., 22.],
        [43., 50.]])
torch.matmul 同样可用:
 tensor([[19., 22.],
        [43., 50.]])

动手实践

  1. 手算 ,用「线性组合」视角写清楚每一步,再用代码验证。
  2. 手算 ,并说出这个矩阵对向量做了什么变换。
  3. 计算 ,其中 ,观察顺序的影响。
  4. 用 PyTorch 验证结合律:随机生成三个 2×2 矩阵,比较 (A @ B) @ CA @ (B @ C)
  5. 修改 7.4 的代码,把 换成「水平翻转」矩阵 ,观察复合变换的效果。

常见错误

错误写法/理解原因
当成逐元素相乘矩阵乘法是行×列(线性组合),不是 A * B;逐元素相乘是 Hadamard 积
认为 矩阵乘法不满足交换律,顺序不能换
两个矩阵尺寸不匹配还硬乘内维必须相等: 才合法
写成 复合顺序:先 ,合并成 ,不是
NumPy 里 A * BA @ B 混淆* 是逐元素乘,@ 才是矩阵乘法

章末练习

基础

  1. 手算并验证:
  2. 判断下列乘法是否合法,并给出结果的形状:
  3. 用 NumPy 验证 与「列向量线性组合」的等价性,任选一组

提高

  1. 已知 ,手算 ,比较结果并指出哪个位置不同。
  2. 证明:单位矩阵满足 (对 2×2 逐元素写出证明)。

挑战

  1. 证明结合律 (对 逐元素展开 ,利用求和可以交换顺序)。
  2. 找两个 2×2 矩阵 ,使得 (零矩阵)但 。这说明什么?

章末自测

  1. ,则 的形状是?
    • A.
    • B.
    • C.
    • D. 无法相乘
    • A.
    • B.
    • C.
    • D.
  2. 矩阵乘法 的第 行第 列等于?
    • A. 行与 列的点积
    • B. 行与 列的点积
    • C.
    • D.
  3. 下列哪条对矩阵乘法成立?
    • A.
    • B.
    • C.
    • D.
  4. 矩阵 的几何意义是?
    • A. 的列按 分量做线性组合
    • B. 的行按 分量做线性组合
    • C. 的每个分量乘以
    • D. 的点积
  5. 对向量做的变换是?
    • A. 逆时针旋转 90°
    • B. 水平翻转
    • C. 拉伸 2 倍
    • D. 不变
  6. 两个 矩阵相乘,结果一定是?
    • A. 矩阵
    • B. 向量
    • C. 标量
    • D. 矩阵
  7. NumPy 中真正的矩阵乘法运算符是?
    • A. *
    • B. @
    • C. dot 方法
    • D. BC 都对
  8. 合法,则 的列数与 的?相等
    • A. 行数
    • B. 列数
    • C. 元素总数
    • D. 行数加列数
  9. 先做变换 再做变换 ,复合变换的矩阵是?
    • A.
    • B.
    • C.
    • D.