qinghuan
← 数学语言与概率基础 / 全部笔记

单元 03 / 数学语言与概率基础

P0 · 必会主线L0 · 零基础

矩阵与线性变换:看懂形状和乘法

用有行有列的数表理解矩阵乘法,再看一批输入经过线性层时,转置、偏置和空维度怎样处理。

本篇目录 · 按需要跳转

读完这篇,你能做什么

能逐项计算小矩阵乘积,写出线性层各变量的形状,并检查两侧相接维度长度为零时的实现。

学习理由、准备要求与知识点

为什么学它

神经网络里的数字有固定排列方式。先判断矩阵能不能相乘,才能看懂参数怎样存、一批样本怎样一起计算。

先读这些内容

知识点清单

  • 矩阵行列与元素下标
  • 矩阵乘法内维匹配
  • 矩阵向量变换
  • 乘法顺序与非交换性
  • 转置与权重布局
  • 批量维度
  • 线性与仿射变换
  • 零长度轴与计算成本

运行前先确认Java 17;Python 3.14 CPU 教学实验,无外部数据、预训练权重、GPU 或多机训练。源代码基线与局限见源码模块。

01先弄懂它是什么

处理一批数据时,可以把数字排成一张有行有列的表,这就是矩阵。A 的形状写作 m×k,表示 m 行、k 列,A[i,j] 是第 i 行第 j 列的数。做矩阵乘法前先检查行列数:A 为 m×k、B 为 k×n,才能得到 m×n 的结果 C。这里会把一行与一列的对应数相乘后相加,和两张表对应位置直接相乘是两种运算。

矩阵还可以把一组输入算成一组输出。例如把几项学习记录分别乘上权重再相加,就能得到一个新数。若列向量 x 有 k 个分量,A 为 m×k,那么 y=A*x 有 m 个分量,每个输出都是输入的加权和。这种线性变换满足 A*(u+v)=A*u+A*v 和 A*(c*u)=c*(A*u),全零输入会得到全零输出。若再加固定偏置 b,得到 A*x+b,严格名称是仿射变换。

机器学习常把多个样本放在行上:X 的形状为 batch×in_features,线性层权重 W 常存成 out_features×in_features,于是输出 Y=X*W^T+b,形状为 batch×out_features。这里转置来自存储约定,不能只凭变量名 W 猜乘法方向。列向量写法与批量行写法表达同一类计算,但公式位置需要对应调整。

文中这几个词,先认一认

内维
相乘时必须对上的那两个维度。例如 m×k 乘 k×n,中间两个 k 必须相同。
转置
把矩阵的行和列交换位置:A^T[j,i]=A[i,j]。
仿射变换
先做线性变换,再加一个固定的平移量。例如计算加权和后再加偏置。

还有陌生词?到术语小抄里看一个具体例子 ↗

02看看它怎么工作

结果元素 C[i,j] 等于 sum(t=0..k-1,A[i,t]*B[t,j]),也就是 A 的第 i 行与 B 的第 j 列做点积。这样可以从点积直接推导矩阵乘法,而不必记忆黑箱规则。若 A=[[1,2],[3,4]]、B=[[2,0],[0,3]],第一行输出为 [2,6],第二行为 [6,12]。

乘法顺序通常不能交换。右乘对角矩阵会按列缩放,左乘同一个对角矩阵会按行缩放,结果一般不同。即便两种顺序的形状都合法,数值也未必相等;若形状不同,反过来的乘法甚至无法定义。神经网络层的先后顺序因此不能仅因为维度都一样就随意交换。

矩阵结合律在实数数学中允许改变括号,例如 (A*B)*C=A*(B*C),但计算量和中间矩阵大小可能差很多。浮点计算还会因累加顺序不同产生舍入差异,所以实际结果通常只能在容差内比较。选择计算顺序时既要考虑形状成本,也要认识数值表示与精确代数的差别。

形状中出现零并不自动代表非法。A 为 2×0、B 为 0×3 时,每个输出元素是空求和,按约定为零,结果是 2×3 的全零矩阵。只有另外保存行数和列数,才能区分 0×3 与 0×8;只给一个空二维列表,无法知道原来有多少列。Java 算法保存这些行列信息,Python 小实验则明确只接收非空矩形列表。

朴素三重循环对每个 m*n 输出做 k 次乘加,时间为 O(m*k*n),输出存储为 O(m*n)。真实库会使用缓存友好的分块、向量化和优化后端,甚至把加偏置融合进乘法。理论乘加次数可以帮助比较方案,却不能单独预测 CPU 或 GPU 上的实际耗时。

先跑通一个最小例子

Python stdlib 实验使用小型非空矩形列表,验证乘法、单位矩阵和非交换性。它没有调用 BLAS 或 GPU。

experiment.py · python
def matmul(a,b):
    if not a or not b or not a[0] or not b[0]:
        raise ValueError("nonempty matrices only")
    k=len(a[0]); n=len(b[0])
    if any(len(row)!=k for row in a) or any(len(row)!=n for row in b):
        raise ValueError("ragged")
    if k!=len(b):
        raise ValueError("inner dimension")
    # 每个结果由一行与一列的点积形成。
    return [[sum(a[i][t]*b[t][j] for t in range(k)) for j in range(n)] for i in range(len(a))]
a=[[1,2],[3,4]]; eye=[[1,0],[0,1]]; scale=[[2,0],[0,3]]
assert matmul(a,eye)==a
assert matmul(a,scale)==[[2,6],[6,12]]
assert matmul(scale,a)==[[2,4],[9,12]]  # 左右乘同一个矩阵并不相同。
assert matmul(a,scale)!=matmul(scale,a)
assert matmul([[2,3]],[[4],[5]])==[[23]]
try:
    matmul([[1,2]],[[1,2]])
    raise AssertionError("mismatch accepted")
except ValueError:
    pass
print("PASS 6: identity, row/column scaling, order, dot product, invalid shape")

核对运行结果打印 PASS 6;A*scale 与 scale*A 不相等。实验限定非空列表,Java 程序另外验证显式空维度。

正在加载流程图…

先画出每个操作数的形状,再决定转置与广播位置。
查看流程图源码(Mermaid)
flowchart LR
 A["X 为 batch 乘输入维"] --> C["内维对齐做乘加"]
 B["W 转置为输入维乘输出维"] --> C
 C --> D["输出 batch 乘输出维"]
 D --> E["按输出维加入偏置"]

按顺序看,每一步做了什么

  1. 写出两个矩阵的显式行列。
  2. 检查左列数等于右行数。
  3. 确定结果的左行数和右列数。
  4. 按输出位置累加共同内维。
  5. 核对空维度、转置和偏置规则。

03对照实现代码

PyTorch v2.9.1:ATen linear 的矩阵与偏置路径 (新标签页)↗

PyTorch v2.9.1;ATen native Linear.cpp · at::native::linear

以下为原创精简教学实现,不是原样库源码;只演示本文说明的机制,省略真实实现的优化、调度和完整兼容边界。

核心机制代码 · python
def row_linear(x,weight,bias):
    # 原创二维模型:weight 按 [out_features, in_features] 存储。
    batch=len(x); output=len(weight); features=len(weight[0])
    if len(bias)!=output or any(len(row)!=features for row in x+weight):
        raise ValueError("shape")
    return [[sum(x[i][k]*weight[j][k] for k in range(features))+bias[j]
             for j in range(output)] for i in range(batch)]
    # 没有实现 Tensor 设备、自动微分或融合后端。

PyTorch v2.9.1 的 ATen linear 首先检查输入和权重不是零维标量,并根据后端及张量性质选择执行路径。对于二维输入且存在偏置的常见情况,它使用 addmm 将偏置与矩阵乘法组合;这与公式中的批量输入乘权重转置一致。

通用路径调用 matmul(input, weight.t()),然后根据偏置及张量特性选择加法。源码还存在连续多维输入的展平路径和后端特定分支。因此一个数学线性层可以对应多个实际内核,不能把教学三重循环当成框架真正的性能实现。

原创模型把权重的第 j 行与输入第 i 行做点积,再加 bias[j],直接表现 W 为输出维乘输入维的存储布局。它只支持说明中的非空二维列表,省略设备、稀疏布局、自动微分和一般广播检查。真实库源码是 C++,这里展示的是等式的 Python 消费模型。

阅读 linear 时要区分接口维度与内存布局。连续性可能影响是否能廉价展平和选择哪条路径,但转置在语义上仍是交换维度,不一定立刻复制所有元素。后续 Tensor 篇会进一步区分视图与复制,本篇只用形状证明计算关系。

代码先调用谁,再调用谁

  1. 线性层传入 input、weight 与可选 bias
  2. ATen linear 检查维度及后端
  3. 二维偏置路径选择 addmm
  4. 通用路径 matmul(input,weight.t())
  5. 按约定加入偏置并返回输出

04动手写一个例子

保留显式形状的矩阵乘法

有限 double 数据按行优先存储,显式指定行列,允许零长度轴。实现矩阵乘法,拒绝内维不匹配、非法形状和非有限结果。

输入 1
2×2 的 A 乘对角矩阵 diag(2,3)
输出 1
[[2,6],[6,12]]
输入 2
2×0 乘 0×3
输出 2
2×3 全零
输入 3
2×3 乘 2×2
输出 3
拒绝

使用一维数组加行列元数据表示矩阵,元素位置为 row*cols+col。构造时用精确整数乘法检查所需长度,并复制输入数据,避免外部修改破坏已验证的矩阵。保留显式形状,使零行矩阵仍能记住列数。

为每个输出位置创建从零开始的累加器,沿共同内维扫描。内维为零时循环不执行,输出保持零,正好对应空求和。输出位置只写入一次,便于建立每次累加覆盖了哪个前缀的不变量。

输入有限并不保证乘积或累加仍有限,所以实现检查每个中间结果,遇到无法用 double 表示的计算就报错。这比静默返回无穷更容易定位,但也意味着某些数学上可抵消的大数表达式仍可能被拒绝;高级稳定矩阵乘法需要更完整的数值策略。

为什么这样写是对的计算 C[i,j] 的内层第 t 步之后,累加器等于前 t 项 A[i,k]*B[k,j] 的顺序浮点和。

时间 O(m*k*n)。额外空间 O(m*n) 输出,核心循环额外 O(1)。

共有 m*n 个输出,每个输出检查 k 个乘积;矩阵构造复制输入的成本另随输入元素数增长。

保存为 Main.java,运行 javac --release 17 -encoding UTF-8 Main.java,再运行 java Main。程序包含固定输入自测,断言失败时会抛出错误。

展开收起代码 · Main.java · 完整可运行程序Java · 37 行

长行可在代码区域内左右滚动,按 Tab 可把焦点移入代码区。

Main.java · 完整可运行程序 · Java
import java.util.Arrays;
public class Main {
  static final class Mat {
    final int rows,cols; final double[] data;
    Mat(int rows,int cols,double[] source){
      if(rows<0||cols<0||source==null||Math.multiplyExact(rows,cols)!=source.length)throw new IllegalArgumentException("shape");
      for(double x:source)if(!Double.isFinite(x))throw new IllegalArgumentException("finite");
      this.rows=rows;this.cols=cols;this.data=source.clone(); // 固定形状并复制来源。
    }
    double get(int r,int c){return data[r*cols+c];}
  }
  static Mat multiply(Mat a,Mat b){
    if(a.cols!=b.rows)throw new IllegalArgumentException("inner dimension");
    double[] out=new double[Math.multiplyExact(a.rows,b.cols)];
    for(int i=0;i<a.rows;i++)for(int j=0;j<b.cols;j++){
      double sum=0; // 零内维对应空求和,保留零。
      for(int k=0;k<a.cols;k++){
        sum+=a.get(i,k)*b.get(k,j);
        if(!Double.isFinite(sum))throw new ArithmeticException("nonfinite result");
      }
      out[i*b.cols+j]=sum; // 行优先索引使用输出列数。
    }
    return new Mat(a.rows,b.cols,out);
  }
  static void eq(double[] a,double[] b){if(!Arrays.equals(a,b))throw new AssertionError(Arrays.toString(a));}
  static void bad(Runnable r){try{r.run();}catch(IllegalArgumentException e){return;}throw new AssertionError();}
  public static void main(String[] args){
    Mat a=new Mat(2,2,new double[]{1,2,3,4});
    eq(multiply(a,new Mat(2,2,new double[]{2,0,0,3})).data,new double[]{2,6,6,12});
    eq(multiply(a,new Mat(2,2,new double[]{1,0,0,1})).data,a.data);
    eq(multiply(new Mat(2,0,new double[]{}),new Mat(0,3,new double[]{})).data,new double[6]);
    Mat empty=multiply(new Mat(0,2,new double[]{}),a);if(empty.rows!=0||empty.cols!=2)throw new AssertionError();
    bad(()->multiply(a,new Mat(1,1,new double[]{1})));
    bad(()->new Mat(1,2,new double[]{1}));
    System.out.println("PASS 6");
  }
}

这些特殊情况也要试一试

  • 内维不匹配必须拒绝。
  • 零内维结果仍有显式输出形状。
  • 非法数据长度不能靠访问时越界发现。
  • 有限输入仍可能产生非有限中间结果。
进阶选读 · 矩阵链顺序与分块

场景大量不同形状乘法组成计算图。

做法先估算不同括号顺序的乘加次数与中间存储,再使用优化库进行分块和后端选择;比较实际误差及耗时。

复杂度矩阵链规划可用动态规划,典型 O(s^3) 时间、O(s^2) 空间,s 为矩阵数。

好处与代价乘加较少不一定墙钟时间最短,设备布局和传输成本仍需测量。

05项目里怎么用

在数据进入和离开模块时,最好检查形状,并写明每个轴代表样本、时间、特征还是类别。有些维度错误不会报错,因为广播会自动扩展数组,让运算继续进行,但含义已经不对。测试时可以把不同轴设成不同长度;若全设为二,即使误做了转置,也可能看不出形状变化。

使用高性能库时,避免为了匹配公式频繁复制和转置数据。先理解算子接受的布局,再决定是否需要 contiguous 或重排;这些操作可能引入额外内存和带宽成本。本文的朴素实现用于检查公式,不能用它与向量化框架的耗时直接比较算法优劣。

浮点矩阵运算的测试应兼顾形状、数值和输入不被修改三个方面。整数样例可以精确核对,小数样例使用合理容差;大规模数值问题还需检查条件数、累加精度与尺度。一个小矩阵通过只证明相应路径正确,不代表任意规模训练已经稳定。

容易踩的坑

  • 混淆矩阵乘法与逐元素乘法。
  • 把 W 的存储形状直接当作公式列向量形状。
  • 以为 A*B 总等于 B*A。
  • 忽略空矩阵需要额外形状信息。

几种做法,怎么选

表格有四列,窄屏可左右滑动查看代价和适用条件。

方案优势代价与限制适用条件
手写三重循环机制清晰,容易检查边界缺少硬件优化教学和小型参考实现
库矩阵算子后端优化且支持批处理需理解布局和广播契约真实训练与推理
预先变换布局可减少重复转换占额外存储且有维护成本重复使用同一权重布局

06检验是否读懂了

先自己想、自己写,再展开答案核对。场景题是假设的练习情境;遇到还没学过的内容,可以做完基础后再回来。

先用自己的话解释

01m×k 乘 k×n 的结果为何为 m×n?

展开参考答案与推导

每个左侧行与每个右侧列形成一个点积,左边有 m 行、右边有 n 列,因此有 m*n 个输出;共同的 k 维被求和消去。

02加偏置后仍是严格线性映射吗?

展开参考答案与推导

通常不是。非零偏置使零输入得到非零输出,违反线性映射把零映到零的性质;它属于仿射变换。神经网络习惯名称不应替代数学定义。

03为什么权重常需要转置?

展开参考答案与推导

框架常把权重存为输出维乘输入维,而批量输入按样本行排列。要让输入维作为内维相接,就使用权重转置,具体方向取决于约定。

换个情况,还会处理吗

01维度相等但结果明显不对怎么办?

展开参考答案与推导

检查每个轴的语义、权重是否转置以及是否意外采用逐元素乘法。用不对称形状和可手算数据定位,不能只靠没有维度异常判断正确。

02两个括号顺序输出末位不同正常吗?

展开参考答案与推导

浮点加法乘法有舍入,改变计算顺序可能导致小差异。应在合理容差内比较并检查误差规模,但不能以浮点误差为由忽略巨大或结构性偏差。

03同一公式在库中为何有多条路径?

展开参考答案与推导

后端、连续性、输入维度和偏置会影响融合与布局处理。它们可能实现相同数学契约,却具有不同的性能和数值细节。

写代码试一试

01推断线性层形状。

输入 / 条件
X 为 4×3,W 为 2×3
输出 / 预期
Y 为 4×2

提示 权重转置。

展开参考答案与推导

计算 X*W^T,使共同的三维输入相接;再按两个输出特征加偏置。直接 X*W 的内维并不匹配。

02手算矩阵列缩放。

输入 / 条件
[[1,2],[3,4]] 右乘 diag(2,3)
输出 / 预期
[[2,6],[6,12]]

提示 每列乘对应对角值。

展开参考答案与推导

第一列乘二,第二列乘三。若改成左乘则按行缩放,结果会不同,这可以作为识别乘法顺序的测试。

03处理空内维。

输入 / 条件
2×0 乘 0×3
输出 / 预期
2×3 全零

提示 空求和为零。

展开参考答案与推导

保留行列元数据,为六个输出位置使用初始零;内层没有项,不执行乘加。不能把结果误认为没有行列信息的空列表。

读完之后,独立完成

课后作业:自己做一遍

本篇共 10 道作业,先完成 8 道基础与综合任务,再按前置知识挑战进阶题。其中包含 1 道思维拓展,试着换个条件,看看原来的办法是否还管用。不附参考答案。

这次练什么

能逐项计算小矩阵乘积,写出线性层各变量的形状,并检查两侧相接维度长度为零时的实现。

本篇重点加练

线性映射的结构约束比单次乘法更重要,可用信息丢失和输入变化检验。

  • 矩阵向量变换
  • 线性与仿射变换

重点内容会多练几次。先说清楚怎么做,再试试空数据、出错或条件改变时会发生什么。

先自己做,卡住时记下问题,再回来查。问答题写清“为什么、什么时候适用”;动手题留下输入、输出和运行步骤,方便下次重做。

基础必做

  1. 作业 01代码推演

    给定 A=[[1,2],[0,-1],[3,0]]、x=[2,-1],手算 Ax;为每个输出写出所访问的行列下标,并判断 xA 是否有定义。

    提交内容
    逐项计算及形状表
    怎样算完成
    • 下标与形状一致
    • 未定义运算说明明确

    练到的知识点矩阵行列与元素下标 · 矩阵乘法内维匹配 · 矩阵向量变换

  2. 作业 02原理问答

    自行构造两个二维线性变换,比较交换执行顺序后的结果;再加入非零偏置,检验映射是否满足线性性的两个条件。

    提交内容
    矩阵、输入和检验记录
    怎样算完成
    • 反例可复核
    • 线性条件逐项检查

    练到的知识点乘法顺序与非交换性 · 线性与仿射变换

  3. 作业 03原理问答

    对 F(x)=Ax+b,自拟二维 A、b,比较 F(u+v)、F(u)+F(v) 与输入同时平移后的输出;写出哪些结论依赖 b 的取值。

    提交内容
    条件化等式与例子
    怎样算完成
    • 线性与仿射条件明确
    • 单个例子不代替一般论证

    练到的知识点矩阵向量变换 · 线性与仿射变换

  4. 作业 04代码推演

    给 A=[[1,1],[2,2]],寻找是否存在两个不同输入产生相同输出;再审查“知道输出就能还原输入”的接口说明。

    提交内容
    输入对与接口审阅
    怎样算完成
    • 输入确实不同且输出可核对
    • 结论针对给定变换

    练到的知识点矩阵向量变换

综合应用

  1. 作业 05编程实践

    仅用 Python 标准库实现批量仿射层,输入 [B,4]、权重 [3,4]、偏置 [3],取 B=2;与独立手算记录核对同一组参数。

    提交内容
    CPU 实现与形状测试
    怎样算完成
    • 权重布局显式
    • 批量维度不会误作特征维度

    练到的知识点转置与权重布局 · 批量维度

  2. 作业 06排错实验

    两个 4×4 权重分别表示输入到输出和输出到输入。构造形状检查无法发现的方向错误,记录错误实现与契约之间的差异。

    提交内容
    最小复现与诊断报告
    怎样算完成
    • 形状相同仍能检出语义错误
    • 测试输入有辨识力

    练到的知识点矩阵乘法内维匹配 · 转置与权重布局 · 批量维度

  3. 作业 07设计评审

    评审矩阵乘法接口对 [2,0]×[0,3]、[0,2]×[2,4] 和不规则二维列表的处理,写出验收标准并实现测试。

    提交内容
    输入契约与可运行测试
    怎样算完成
    • 空轴语义明确
    • 非法结构不会静默接受

    练到的知识点零长度轴与计算成本

  4. 作业 08编程实践

    只用 Python 标准库实现两份二维仿射程序的等价性检查实验:在自己选的有限输入点比较结果,并给出额外的参数层审计。

    提交内容
    程序与两层检查记录
    怎样算完成
    • 采样检查范围明确
    • 参数与输入身份一致

    练到的知识点矩阵向量变换 · 线性与仿射变换

进阶挑战 · 掌握前置后选做

  1. 作业 09面试场景

    矩阵链形状依次为 [20,400]、[400,5]、[5,100]。比较两种括号方式的乘加次数和中间存储,并设计 CPU 小实验验证代价模型的局限。

    提交内容
    推导与测量记录
    怎样算完成
    • 中间量计算完整
    • 理论操作数与实测时间分开

    练到的知识点矩阵乘法内维匹配 · 乘法顺序与非交换性 · 零长度轴与计算成本

思维拓展

先写出你的猜想,再用例子或实验验证。答案可以不同,但要说清依据、适用情况,以及还有什么没弄懂。

  1. 作业 10思维拓展 · 跨域迁移

    把仿射变换迁移到摄氏/华氏温标或图像坐标变换,研究连续执行两次变换时偏置如何参与;自己定义完整单位与坐标约定。

    提交内容
    跨域模型及组合验证
    怎样算完成
    • 坐标原点和单位明确
    • 组合关系经独立例子核验

    练到的知识点矩阵向量变换 · 线性与仿射变换

完成后,再检验一次

给每题标记“独立完成 / 查阅后完成 / 尚未完成”。查过资料或没做出的题,记下卡在哪里。隔一天换一组输入再试,能解释结果和出错原因,再把这个知识点标为掌握。

继续阅读

先看能帮你完成当前练习的部分。例子跑通后,再读更深入的解释。

打开资料库,找下一份学习材料 ↗