读完这篇,你能做什么
能逐项计算小矩阵乘积,写出线性层各变量的形状,并检查两侧相接维度长度为零时的实现。
学习理由、准备要求与知识点
为什么学它
神经网络里的数字有固定排列方式。先判断矩阵能不能相乘,才能看懂参数怎样存、一批样本怎样一起计算。
先读这些内容
知识点清单
- 矩阵行列与元素下标
- 矩阵乘法内维匹配
- 矩阵向量变换
- 乘法顺序与非交换性
- 转置与权重布局
- 批量维度
- 线性与仿射变换
- 零长度轴与计算成本
运行前先确认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。
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["按输出维加入偏置"]按顺序看,每一步做了什么
- 写出两个矩阵的显式行列。
- 检查左列数等于右行数。
- 确定结果的左行数和右列数。
- 按输出位置累加共同内维。
- 核对空维度、转置和偏置规则。
03对照实现代码
PyTorch v2.9.1;ATen native Linear.cpp · at::native::linear
以下为原创精简教学实现,不是原样库源码;只演示本文说明的机制,省略真实实现的优化、调度和完整兼容边界。
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 篇会进一步区分视图与复制,本篇只用形状证明计算关系。
代码先调用谁,再调用谁
- 线性层传入 input、weight 与可选 bias
- ATen linear 检查维度及后端
- 二维偏置路径选择 addmm
- 通用路径 matmul(input,
weight.t()) - 按约定加入偏置并返回输出
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] 的顺序浮点和。
共有 m*n 个输出,每个输出检查 k 个乘积;矩阵构造复制输入的成本另随输入元素数增长。
保存为 Main.java,运行 javac --release 17 -encoding UTF-8 Main.java,再运行 java Main。程序包含固定输入自测,断言失败时会抛出错误。
展开收起代码 · Main.java · 完整可运行程序
长行可在代码区域内左右滚动,按 Tab 可把焦点移入代码区。
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 道思维拓展,试着换个条件,看看原来的办法是否还管用。不附参考答案。
能逐项计算小矩阵乘积,写出线性层各变量的形状,并检查两侧相接维度长度为零时的实现。
本篇重点加练
线性映射的结构约束比单次乘法更重要,可用信息丢失和输入变化检验。
- 矩阵向量变换
- 线性与仿射变换
重点内容会多练几次。先说清楚怎么做,再试试空数据、出错或条件改变时会发生什么。
先自己做,卡住时记下问题,再回来查。问答题写清“为什么、什么时候适用”;动手题留下输入、输出和运行步骤,方便下次重做。
基础必做
- 作业 01代码推演
给定 A=[[1,2],[0,-1],[3,0]]、x=[2,-1],手算 Ax;为每个输出写出所访问的行列下标,并判断
xA是否有定义。- 提交内容
- 逐项计算及形状表
- 怎样算完成
- 下标与形状一致
- 未定义运算说明明确
练到的知识点矩阵行列与元素下标 · 矩阵乘法内维匹配 · 矩阵向量变换
- 作业 02原理问答
自行构造两个二维线性变换,比较交换执行顺序后的结果;再加入非零偏置,检验映射是否满足线性性的两个条件。
- 提交内容
- 矩阵、输入和检验记录
- 怎样算完成
- 反例可复核
- 线性条件逐项检查
练到的知识点乘法顺序与非交换性 · 线性与仿射变换
- 作业 03原理问答
对
F(x)=Ax+b,自拟二维 A、b,比较F(u+v)、F(u)+F(v)与输入同时平移后的输出;写出哪些结论依赖 b 的取值。- 提交内容
- 条件化等式与例子
- 怎样算完成
- 线性与仿射条件明确
- 单个例子不代替一般论证
练到的知识点矩阵向量变换 · 线性与仿射变换
- 作业 04代码推演
给 A=[[1,1],[2,2]],寻找是否存在两个不同输入产生相同输出;再审查“知道输出就能还原输入”的接口说明。
- 提交内容
- 输入对与接口审阅
- 怎样算完成
- 输入确实不同且输出可核对
- 结论针对给定变换
练到的知识点矩阵向量变换
综合应用
- 作业 05编程实践
仅用 Python 标准库实现批量仿射层,输入 [B,4]、权重 [3,4]、偏置 [3],取 B=2;与独立手算记录核对同一组参数。
- 提交内容
- CPU 实现与形状测试
- 怎样算完成
- 权重布局显式
- 批量维度不会误作特征维度
练到的知识点转置与权重布局 · 批量维度
- 作业 06排错实验
两个 4×4 权重分别表示输入到输出和输出到输入。构造形状检查无法发现的方向错误,记录错误实现与契约之间的差异。
- 提交内容
- 最小复现与诊断报告
- 怎样算完成
- 形状相同仍能检出语义错误
- 测试输入有辨识力
练到的知识点矩阵乘法内维匹配 · 转置与权重布局 · 批量维度
- 作业 07设计评审
评审矩阵乘法接口对 [2,0]×[0,3]、[0,2]×[2,4] 和不规则二维列表的处理,写出验收标准并实现测试。
- 提交内容
- 输入契约与可运行测试
- 怎样算完成
- 空轴语义明确
- 非法结构不会静默接受
练到的知识点零长度轴与计算成本
- 作业 08编程实践
只用 Python 标准库实现两份二维仿射程序的等价性检查实验:在自己选的有限输入点比较结果,并给出额外的参数层审计。
- 提交内容
- 程序与两层检查记录
- 怎样算完成
- 采样检查范围明确
- 参数与输入身份一致
练到的知识点矩阵向量变换 · 线性与仿射变换
进阶挑战 · 掌握前置后选做
- 作业 09面试场景
矩阵链形状依次为 [20,400]、[400,5]、[5,100]。比较两种括号方式的乘加次数和中间存储,并设计 CPU 小实验验证代价模型的局限。
- 提交内容
- 推导与测量记录
- 怎样算完成
- 中间量计算完整
- 理论操作数与实测时间分开
练到的知识点矩阵乘法内维匹配 · 乘法顺序与非交换性 · 零长度轴与计算成本
思维拓展
先写出你的猜想,再用例子或实验验证。答案可以不同,但要说清依据、适用情况,以及还有什么没弄懂。
- 作业 10思维拓展 · 跨域迁移
把仿射变换迁移到摄氏/华氏温标或图像坐标变换,研究连续执行两次变换时偏置如何参与;自己定义完整单位与坐标约定。
- 提交内容
- 跨域模型及组合验证
- 怎样算完成
- 坐标原点和单位明确
- 组合关系经独立例子核验
练到的知识点矩阵向量变换 · 线性与仿射变换
完成后,再检验一次
给每题标记“独立完成 / 查阅后完成 / 尚未完成”。查过资料或没做出的题,记下卡在哪里。隔一天换一组输入再试,能解释结果和出错原因,再把这个知识点标为掌握。
继续阅读
先看能帮你完成当前练习的部分。例子跑通后,再读更深入的解释。
- 必读参考文档英文
PyTorch v2.9.1:ATen linear 的矩阵与偏置路径 (新标签页)↗
核对 linear 中
weight.t、addmm、通用 matmul 及连续输入分支,理解形状与实际内核的关系。 - 深入参考文档英文
MIT 18.06:线性代数 (新标签页)↗
阅读矩阵乘法、线性变换与转置课程。
- 深入参考文档英文
PyTorch 2.9 Linear (新标签页)↗
核对输入、权重、偏置和输出形状。
- 深入参考文档英文
PyTorch 2.9 matmul (新标签页)↗
了解矩阵、向量和批量维度的不同规则。