目标:理解行副本、二维索引和累计值的作用域。
修改:prep.py中的copy_matrix、matmul。
预计投入:45–90 分钟;矩阵乘法只用你已经会的手算规则。
1. 一张矩阵在 Python 中是什么
本节路线:第1–2节修复prep q2的copy_matrix,第3–5节修复同题的matmul;先修为列表循环和预备1的状态追踪。两者都完成后再跑q2全部测试。命令沿用 课程根目录。出口应有一幅行别名图、一格点积、一个非方阵新例子,而不是要求你现在掌握GPU二维布局。
a = [[1, 2], [3, 4]]
外层列表放两行,内层列表放各行的元素。a[1] 是 [3,4],a[1][0] 是 3。两个索引都从 0 开始。
这里讨论 Python 对象,不把嵌套列表当作 GPU 的连续二维存储。后面计算 word 地址时,会明确换成一维布局。
2. 先预测:只复制外层够不够?
a = [[1, 2], [3, 4]]
b = a[:]
b[0][0] = 99
print(a)
先预测,再在交互窗口试。a[:] 建了新外层,但其中的行仍然指向原来的行:
a[0] ──→ [1, 2] ←── b[0]
a[1] ──→ [3, 4] ←── b[1]
所以改 b[0][0] 会改到 a[0][0]。copy_matrix 的规格要求每一行都独立,因此要逐行复制。本课只处理数值矩阵,行中的数值不需要再递归复制。
复制题提示
遍历原矩阵的每一行,给每行做 row[:],将新行放进一个新外层列表。可以用循环,不要求会列表推导式。
3. 手算四格,再看三层循环
A = [[1, 2], B = [[5, 6],
[3, 4]] [7, 8]]
C[0][0] = 1*5 + 2*7 = 19
C[0][1] = 1*6 + 2*8 = 22
C[1][0] = 3*5 + 4*7 = 43
C[1][1] = 3*6 + 4*8 = 50
将“取 A 的一行和 B 的一列”翻译成索引:
A[i][p] * B[p][j]
i 决定输出行,j 决定输出列,p 扫过相乘相加的那条维度。A 的形状为 (m,k),B 为 (k,n),输出为 (m,n);两个 k 必须相同。
4. 累计值应在哪里清零?
starter 把 total=0 放在所有循环之前。先别改,跟踪第一行:
| 输出格 | 进入该格前 total | 本格应加的值 | 错误结果 | 正确结果 |
|---|---|---|---|---|
| C[0][0] | 0 | 19 | 19 | 19 |
| C[0][1] | 19 | 22 | 41 | 22 |
第一格正确,第二格开始累积了别的格子的结果。想清楚:total 代表整张矩阵的和,还是当前一个输出格的点积?
矩阵乘法提示 1
只要开始计算一个新的 C[i][j],就应有一个新的累计值。
矩阵乘法提示 2
把初始化移到 j 循环里面、p 循环之前。不要放在 p 循环里,否则每次乘法都会抹掉前面的和。
5. 运行、定位、再检查
python course/tools/grade.py prep --question q2 --failfast
如果失败的是副本测试,先修 copy_matrix;如果是数值测试,打印一个错误格子的 i,j,p,total。本题只要求非空矩形输入,不要求在这一步写一个完整的张量库。
也不要用 [[0]*n]*m 创建独立输出行:它会重复引用同一行。给每一行单独创建列表。
6. 独立检验
手算 [[1,2,3]] @ [[1],[0],[-1]],预测形状和结果,再调用函数对照。然后解释为什么下式不合法:[[1,2]] @ [[3,4]]。
检查自己的答案
合法例子的形状是 (1,3) @ (3,1) → (1,1),结果 [[-2]]。另一个例子是 (1,2) @ (1,2),内维 2 和 1 不匹配,要求抛 ValueError。
下一步:预备 3:让测试帮助你。