▤书架 ← 返回索引
▶目录

阶段 15 讲义

数学 11h:线代 · 特征值(二)与对角化
编程 9h:Git 进阶与调试器


第一部分 · 数学

阶段 13 学会了"找出特征值和特征向量"。这一阶段回答:找它们有什么用?

答案:让矩阵变简单。

一个矩阵可能很复杂,但如果你"换到特征向量这组基"去看,它就变成了一个对角矩阵。
对角矩阵是矩阵世界里最简单的东西:每个方向各自拉伸,互不干涉。

1 相似矩阵:换一组基,本质不变

同一个线性变换,换一组基,矩阵会变,但"本质"不变。

$$B = P^{-1}AP$$

读法:$A$ 是"在旧基下的样子",$B$ 是"在新基下的样子",$P$ 是"换基的翻译器"。

$A$ 和 $B$ 相似。相似矩阵有相同的不变量:

不变量 为什么重要
特征值完全相同 特征值是变换本身的属性,和基无关
行列式相同 $\det(P^{-1}AP)=\det A$
迹相同 $\text{tr}(P^{-1}AP)=\text{tr}(A)$
秩相同 独立方向个数不变

⭐ "相似"这个词很贴切:长得不一样,但是同一个东西的两种描述。
就像同一个人穿不同衣服,衣服变了,人没变。

2 对角化:找到"最简单的那个描述"

问题:在所有相似的矩阵里,最简单的是哪个?

答案:对角矩阵。因为 $D = \begin{pmatrix}\lambda_1&&\\&\ddots&\\&&\lambda_n\end{pmatrix}$ 作用在一个 vector 上就是"每个分量各自乘一个数",没有任何耦合。

对角化就是找 $P$ 使得:

$$\boxed{A = P\Lambda P^{-1}} \quad \text{或等价地} \quad \boxed{\Lambda = P^{-1}AP}$$

其中 $\Lambda$ 是对角矩阵。

2.1 关键在于:$P$ 是什么?

$P$ 的每一列就是 $A$ 的特征向量,$\Lambda$ 的对角线就是对应的特征值。

为什么:把 $Av_i = \lambda_i v_i$ 按列拼起来:

$$A\underbrace{[v_1\ v_2\ \cdots\ v_n]}_{P} = \underbrace{[v_1\ v_2\ \cdots\ v_n]}_{P}\underbrace{\begin{pmatrix}\lambda_1&&\\&\ddots&\\&&\lambda_n\end{pmatrix}}_{\Lambda}$$

左边 = $AP$,右边 = $P\Lambda$,所以 $AP = P\Lambda$,即 $A = P\Lambda P^{-1}$。 ✓

2.2 对角化的条件

$$\boxed{A \text{ 可对角化} \iff A \text{ 有 } n \text{ 个线性无关的特征向量}}$$

对 $n$ 阶矩阵:特征向量要"够数"。

情况 可对角化?
$n$ 个互不相同的特征值 ✓ 一定可以
有重根,但几何重数 = 代数重数 ✓ 可以
有重根,且几何重数 < 代数重数 ✗ 不可以

用阶段 13 的例子核对(sympy 实算):

矩阵 特征值 独立特征向量数 可对角化
$\begin{pmatrix}2&1\\1&2\end{pmatrix}$ $3,\ 1$ $2$ ✓
$\begin{pmatrix}2&0\\0&2\end{pmatrix}$ $2$(重根) $\mathbf{2}$ ✓
$\begin{pmatrix}2&1\\0&2\end{pmatrix}$ $2$(重根) $\mathbf{1}$ ✗
$\begin{pmatrix}1&1\\0&2\end{pmatrix}$ $2,\ 1$ $2$ ✓

⚠️ 第二、三行是关键对照:特征多项式完全相同(都是 $(\lambda-2)^2$),
可一个能对角化一个不能。
原因:$\begin{pmatrix}2&0\\0&2\end{pmatrix}$ 是"所有方向都是特征方向",
而 $\begin{pmatrix}2&1\\0&2\end{pmatrix}$ 只有一个特征方向。

2.3 完整例子

把 $A = \begin{pmatrix}2&1\\1&2\end{pmatrix}$ 对角化。

第一步:求特征值(阶段 13 算过)

$$\det(A-\lambda I) = (\lambda-3)(\lambda-1) = 0 \Longrightarrow \lambda_1 = 1,\ \lambda_2 = 3$$

第二步:求特征向量(每个 $\lambda$ 回代)

$$\lambda_1 = 1: v_1 = \begin{pmatrix}-1\\1\end{pmatrix}, \qquad \lambda_2 = 3: v_2 = \begin{pmatrix}1\\1\end{pmatrix}$$

第三步:拼出 $P$ 和 $\Lambda$

$$P = \begin{pmatrix}-1&1\\1&1\end{pmatrix}, \qquad \Lambda = \begin{pmatrix}1&0\\0&3\end{pmatrix}$$

第四步:验算(sympy 实算)

$$P\Lambda P^{-1} = \begin{pmatrix}2&1\\1&2\end{pmatrix} = A \quad ✓$$

⚠️ $P$ 的列顺序必须和 $\Lambda$ 的对角线顺序对应!
第 1 列 $v_1$ 配 $\lambda_1 = 1$,第 2 列 $v_2$ 配 $\lambda_2 = 3$。
搞反了等式就不成立。

3 应用一:算矩阵的幂(对角化最大的用处)

直接算 $A^{10}$ 要乘 10 次,很烦。用对角化就很简单:

$$A = P\Lambda P^{-1} \Longrightarrow A^k = \underbrace{(P\Lambda P^{-1})(P\Lambda P^{-1})\cdots}_{k\text{ 个}}$$

中间那些 $P^{-1}P$ 都变成 $I$,链子塌缩:

$$\boxed{A^k = P\Lambda^k P^{-1}}$$

而对角矩阵的幂极其简单(每个对角线元素各自乘方):

$$\Lambda^k = \begin{pmatrix}\lambda_1^k&&\\&\ddots&\\&&\lambda_n^k\end{pmatrix}$$

3.1 验证

$$\begin{pmatrix}2&1\\1&2\end{pmatrix}^5$$

直接算(sympy):

$$\begin{pmatrix}122&121\\121&122\end{pmatrix}$$

用对角化:$P\Lambda^5P^{-1}$,其中 $\Lambda^5 = \begin{pmatrix}1&0\\0&243\end{pmatrix}$

结果完全相同 ✓

💡 这就是对角化的全部价值:
把一个"要算 $k$ 次矩阵乘法"的问题,变成"算 $k$ 次标量乘方"。
计算量从 $O(kn^3)$ 降到 $O(n^3 + kn)$。

3.2 漂亮的例子:Fibonacci 数列

Fibonacci 数列 $1,1,2,3,5,8,13,\dots$ 满足 $F_{n+1} = F_n + F_{n-1}$。

写成矩阵形式:

$$\begin{pmatrix}F_{n+1}\\F_n\end{pmatrix} = \underbrace{\begin{pmatrix}1&1\\1&0\end{pmatrix}}_{F}\begin{pmatrix}F_n\\F_{n-1}\end{pmatrix}$$

所以:

$$\begin{pmatrix}F_{n+1}\\F_n\end{pmatrix} = F^n\begin{pmatrix}1\\0\end{pmatrix}$$

$F$ 的特征值(sympy 实算):

$$\det(F-\lambda I) = \lambda^2 - \lambda - 1 = 0 \Longrightarrow \lambda = \frac{1\pm\sqrt5}{2}$$

其中 $\frac{1+\sqrt5}{2} = 1.618033988...$ 就是黄金比例 $\varphi$!

实算验证 $F^{10}$:

$$F^{10} = \begin{pmatrix}89&55\\55&34\end{pmatrix} \Longrightarrow F_{11} = 89,\ F_{10} = 55 \quad ✓$$

$F^{20} = \begin{pmatrix}10946&6765\\6765&4181\end{pmatrix}$,对应 $F_{21}=10946,\ F_{20}=6765$ ✓

⭐ 这个例子很美:黄金比例藏在一个"1 和 1"的简单递推里。
它之所以出现,是因为 $\varphi$ 是矩阵 $F$ 特征方程的根,而 $F$ 的特征值决定了 $F^n$ 的增长速度:
当 $n$ 大时,$F_n \approx \frac{\varphi^n}{\sqrt5}$。

这就是"用特征值刻画增长"的经典范例,也是为什么特征值在物理、生态、经济里无处不在,它决定了系统的长期行为。

4 应用二:实对称矩阵必可正交对角化 ⭐

实对称矩阵($A^T = A$)有特别好的性质:

$$\boxed{\text{实对称矩阵一定可对角化,且特征向量可以选成两两正交的}}$$

更强的结论:存在正交矩阵 $Q$($Q^TQ = I$,即 $Q^{-1} = Q^T$),使

$$A = Q\Lambda Q^T$$

好处:不用求逆! 因为 $Q^{-1} = Q^T$,求逆就是转置,计算上稳定得多。

4.1 完整例子

$$A = \begin{pmatrix}2&1\\1&2\end{pmatrix}$$

(它是对称的)

sympy 实算结果:

$$Q = \begin{pmatrix}-\frac{\sqrt2}{2}&\frac{\sqrt2}{2}\\[4pt]\frac{\sqrt2}{2}&\frac{\sqrt2}{2}\end{pmatrix}, \qquad \Lambda = \begin{pmatrix}1&0\\0&3\end{pmatrix}$$

验证:

$$Q^TQ = \begin{pmatrix}1&0\\0&1\end{pmatrix} \quad ✓ \qquad Q^TAQ = \begin{pmatrix}1&0\\0&3\end{pmatrix} \quad ✓$$

(以上都是 sympy 实算结果)

⭐ 为什么这条定理重要:
你在实际数据里遇到的矩阵,多半是对称的:
协方差矩阵、相关矩阵、距离矩阵、核矩阵,全都是对称的。

所以 PCA 可以用稳定又快的 np.linalg.eigh(eigh 里的 h = Hermitian,即对称):
它保证了特征值是实数、特征向量正交。

⚠️ 注意:可对角化 $\ne$ 可正交对角化。
正交对角化是更强的条件,只有对称矩阵(及少数特殊情形)才有。
一般矩阵可对角化,但 $P$ 不一定是正交的。

5 常见的坑

坑 说明
以为所有矩阵都能对角化 $\begin{pmatrix}2&1\\0&2\end{pmatrix}$ 就不行
$P$ 的列顺序和 $\Lambda$ 顺序不匹配 必须一一对应,搞反等式不成立
把 $P^{-1}$ 写成 $P^T$ 只有正交矩阵才有 $P^{-1}=P^T$,一般矩阵不行
忘了验证 $P$ 可逆 特征向量必须线性无关,否则 $P^{-1}$ 不存在
以为相似矩阵"相等" 相似是"换基后的样子",不是相等
以为特征值相同就一定相似 反例:$\begin{pmatrix}2&0\\0&2\end{pmatrix}$ 和 $\begin{pmatrix}2&1\\0&2\end{pmatrix}$,特征值相同但不相似
算 $A^k$ 时把 $\Lambda^k$ 算错 对角矩阵幂是每个对角元素各自乘方,不是整体乘方
对非对称矩阵用 eigh eigh 假设对称,喂给它非对称矩阵会静默给出错误结果

⚠️ 最后一条特别危险:np.linalg.eigh 只看下三角(或上三角),
你给它一个非对称矩阵,它不会报错,而是当作对称矩阵处理,结果完全错。
规则:对称用 eigh(快且稳),不确定就用 eig。

6 对角化和"变换的长期行为"

这一节是阶段 16 的引子。

$A^k = P\Lambda^kP^{-1}$ 告诉你一件事:当 $k$ 很大时,

$$A^k \approx \lambda_{\max}^k \cdot (\text{某个固定方向})$$

因为最大的那个 $\lambda$ 会"碾压"其他所有项($\lambda_{\max}^k$ 增长最快)。

所以:

最大的 $|\lambda|$ 长期行为
$> 1$ 系统爆炸式增长
$= 1$ 系统趋于稳定
$< 1$ 系统衰减到 0

⭐ "最大特征值决定长期行为"是动态系统分析的万能钥匙:
种群增长、传染病 $R_0$、神经网络训练的稳定性、马尔可夫链收敛,全都在问同一个问题:最大的特征值是多少?


7 🧠 这跟认知科学有什么关系

这一节的对应关系是我验证过的(用代码实算),但心理学结论请对照文献。

马尔可夫链的稳态分布 = 特征值 1 对应的特征向量。这条定理在认知建模里到处出现。

7.1 什么是马尔可夫链

一串状态,下一个状态只取决于当前状态,与更早的历史无关(无记忆性)。

认知科学里的例子:

现象 状态
眼动 注视点在不同区域之间跳转
注意切换 注意力在刺激间转移
反应序列 上一个反应影响下一个
语言生成 词与词的转移
决策 在两个选项之间来回
脑状态 静息态网络的切换

7.2 稳态分布是特征向量(实算验证)

我构造了一个"注意力在三个刺激间切换"的转移矩阵:

$$P = \begin{pmatrix}0.7&0.2&0.1\\0.3&0.5&0.2\\0.2&0.3&0.5\end{pmatrix}$$

$P_{ij}$ = 从状态 $i$ 转到状态 $j$ 的概率(每行加起来是 1)。

它的特征值(numpy 实算):

$$1.000000,\quad 0.461803,\quad 0.238197$$

注意那个 $1.000000$。马尔可夫链一定有一个特征值 1(这是数学定理)。

对应的特征向量(归一化后):

$$\pi = (0.463415,\ 0.317073,\ 0.219512)$$

我用另一种方法独立验证:从"一开始一定在状态 1"出发,反复乘转移矩阵 200 次:

$$\text{迭代 200 步后的分布} = (0.463415,\ 0.317073,\ 0.219512)$$

完全一致 ✓

结论:不管从哪里出发,最终都会收敛到这个分布,这就是稳态分布。

7.3 为什么"特征向量"是正确答案

稳态的定义是"再乘一次转移矩阵,分布不变":

$$\pi P = \pi$$

把 $\pi$ 转置成列向量:$P^T\pi^T = \pi^T$,即

$$P^Tv = 1\cdot v$$

这正是"特征值 1 的特征向量"的定义。

⭐ 所以"长期行为"和"特征向量"是同一件事的两种说法:
- 动态视角:反复迭代会收敛到哪?
- 代数视角:特征值 1 的特征向量是什么?

两条路通向同一个答案,这是数学里最美的一类事实。

而且它的唯一性有保证:只要链是"不可约 + 非周期"的,
特征值 1 就是单重的,稳态分布唯一。

7.4 这在认知科学里怎么用

应用 稳态分布代表什么
眼动 / 视觉注意 长期来看注意力花在每个区域的时间比例
语言模型(n-gram) 词频的长期分布
决策模型 长期的选择倾向
认知状态转移 各状态的占据比例
强化学习 策略下的状态访问频率
网络分析(PageRank) 节点的"重要性",就是稳态分布

⭐ PageRank 就是这套东西:把网页当状态、链接当转移,
稳态分布就是每个网页的重要性排名。
你每天用的搜索,底层是这个特征向量问题。

⚠️ 诚实说明:
- 上面的特征值、稳态分布、迭代收敛都是我用 numpy 实算的(可复现),
"马尔可夫链有特征值 1""不可约非周期链的稳态唯一"是数学定理。
- 但"注意力切换能用马尔可夫链建模吗""转移概率怎么估"是研究问题,
而且真实的认知过程很可能不满足"无记忆性","马尔可夫"本身就是一个需要检验的假设,不是默认成立的前提。
请以认知建模的文献为准(如 Computational Modeling of Cognition 类教材)。
我保证数学,不保证模型适用。


8 数学作业 S15

第 10 题参考代码:

import numpy as np

print("=== 作业 4:A^4 的两种算法 ===")
A = np.array([[2., 1.], [1., 2.]])

# 方法一:直接连乘
direct = np.linalg.matrix_power(A, 4)
print("直接矩阵幂:\n", direct)

# 方法二:对角化 A = P Λ P^-1,则 A^k = P Λ^k P^-1
vals, P = np.linalg.eig(A)
Lam = np.diag(vals)
via_diag = P @ np.linalg.matrix_power(Lam, 4) @ np.linalg.inv(P)
print("对角化方法:\n", np.round(via_diag.real, 6))
print("两者一致:", np.allclose(direct, via_diag))

print()
print("=== 作业 5:Fibonacci 矩阵 ===")
F = np.array([[1., 1.], [1., 0.]])
for k in [10, 20]:
    M = np.linalg.matrix_power(F, k)
    print(f"F^{k} =\n{M}")
    print(f"   -> F_{{{k+1}}} = {int(M[0,0])}, F_{k} = {int(M[0,1])}")

vals, _ = np.linalg.eig(F)
print("F 的特征值:", np.round(vals, 6))
print("黄金比例 φ =", (1 + 5**0.5) / 2)

print()
print("=== 作业 7:正交对角化(对称矩阵)===")
A = np.array([[2., 1.], [1., 2.]])
# 对称矩阵用 eigh:更快更稳,特征值自动升序,特征向量正交
vals, Q = np.linalg.eigh(A)
print("特征值:", vals)
print("Q =\n", np.round(Q, 6))
print("Q^T Q = I ?", np.allclose(Q.T @ Q, np.eye(2)))
print("Q^T A Q = Λ ?", np.allclose(Q.T @ A @ Q, np.diag(vals)))

print()
print("=== 第 10 题附加:马尔可夫链稳态 ===")
P_trans = np.array([[0.7, 0.2, 0.1],
                    [0.3, 0.5, 0.2],
                    [0.2, 0.3, 0.5]])

# 方法一:求 P^T 的特征值 1 对应的特征向量
vals, vecs = np.linalg.eig(P_trans.T)
idx = np.argmin(np.abs(vals - 1))
steady = vecs[:, idx].real
steady = steady / steady.sum()
print("特征向量法 稳态:", np.round(steady, 6))

# 方法二:直接迭代到收敛
dist = np.array([1.0, 0.0, 0.0])
for _ in range(200):
    dist = dist @ P_trans
print("迭代法   稳态:", np.round(dist, 6))
print("两者一致:", np.allclose(steady, dist, atol=1e-6))

第二部分 · 编程

9 Git 分支:在"平行世界"里试想法

分支的作用:想试一个新想法,但不想弄坏现在能跑的代码。

阶段 14 的项目能跑。现在你想重构它,万一重构到一半发现不行呢?

分支让你在"平行世界"里改,改好了再合并,改砸了直接丢掉。

9.1 分支的本质(其实非常轻量)

Git 的分支只是一个"指向某次提交的指针",创建分支几乎是零成本。

             main
              │
   A ─── B ─── C          ← 提交历史

git branch feature 只是多了一个指针:

             main
              │
   A ─── B ─── C
              │
           feature

git checkout feature 只是把你"当前所在分支"(HEAD)移动过去。

9.2 常用命令

git branch                    # 列出所有分支(* 表示当前所在)
git branch feature            # 创建分支(不切换)
git checkout -b feature       # 创建并切换(最常用)
git switch -c feature         # 同上,新版命令(推荐)

git switch main               # 切回主干
git switch feature            # 切到 feature

git branch -d feature         # 删除分支(已合并的)
git branch -D feature         # 强制删除(未合并的,⚠️ 会丢工作)

我实跑的演示:

$ git checkout -b feature
$ git branch -v
* feature b2494bd 在分支上的改动
  main    dfb0f5f 初始提交

$ git switch main
$ cat f.txt
line1                          ← 主干上完全看不到分支的改动!

$ git switch feature
$ cat f.txt
line1
feature改动                     ← 切回来,改动还在

⭐ 这就是分支的核心体验:切到哪,文件就变成那个世界的状态。
工作区会跟着分支一起变,不用手工备份文件。

10 合并与冲突

10.1 顺利的合并

$ git switch main
$ git merge feature

Git 自动把 feature 的改动合并进来(我实跑确认):

$ cat f.txt
line1
feature改动

10.2 冲突:两个人改了同一行

冲突发生的原因:Git 不知道你要保留哪个版本。

我实跑制造了一次冲突(main 和 other 都改了 f.txt 的同一行):

$ git merge other
Auto-merging f.txt
CONFLICT (content): Merge conflict in f.txt
Automatic merge failed; fix conflicts and then commit the result.

$ git status --short
UU f.txt                      ← UU = 双方都修改,未解决

冲突文件长这样:

<<<<<<< HEAD
main版本
=======
other版本
>>>>>>> other

三个标记的含义:

标记 含义
<<<<<<< HEAD 下面是你当前分支(main)的版本
======= 分隔线
>>>>>>> other 上面是 other 分支的版本

10.3 解决冲突的步骤

① 打开文件,手工改成你想要的样子

保留两边的改动

(手动删掉三个标记行)

② 标记为已解决

git add f.txt

③ 提交

git commit -m "解决冲突:合并 other 分支"

⚠️ 解决冲突时唯一要记住的原则:
删掉 <<<<<<<、=======、>>>>>>> 这三行标记。
留着它们,你的代码里就会有一堆乱码符号。

⚠️ 不要用 git checkout --ours / --theirs 无脑选一边。
两边可能都有需要保留的东西,冲突往往正是提醒你"这里有重要决定"。

11 远程仓库:把代码放到 GitHub

本地仓库只在你电脑上。远程仓库让代码能备份、能共享、能作为简历。

11.1 首次连接

# 在 GitHub 网站上先建一个空仓库,然后:
git remote add origin git@github.com:你的用户名/仓库名.git
git branch -M main                # 确保主分支叫 main
git push -u origin main           # 首次推送,-u 记住关联

之后就可以简写:

git push          # 推送到远程
git pull          # 拉取远程的更新
git clone <url>   # 把别人的仓库整个复制下来

11.2 fetch vs pull(容易混)

命令 做什么
git fetch 只下载远程的更新,不动你的工作区
git pull fetch + 合并到当前分支

💡 不确定远程改了什么时,先 git fetch 再 git log origin/main 看看,
确认没问题再 git merge。这比直接 pull 稳。

11.3 分支的完整工作流(团队协作的标准姿势)

git switch -c fix-bug          # ① 为新任务开分支
# ...改代码...
git add . && git commit -m "修复 read_records 的空行崩溃"
git push -u origin fix-bug     # ② 推上去
# ③ 在 GitHub 上开 Pull Request(PR),请人 review
# ④ review 通过后,在网页上合并
git switch main && git pull    # ⑤ 本地切回主干并更新
git branch -d fix-bug          # ⑥ 删掉已合并的分支

⭐ "分支 → 改 → PR → 合并"是现代软件开发的标准流程,
也是你以后和实验室师兄协作的方式。

12 撤销操作进阶

Git 的"后悔药"分好几种,用错会丢工作。按危险程度排序:

命令 作用 危险度
git status 只是看看 无
git diff 看改了什么 无
git restore --staged f 从暂存区拿出来 低(文件不变)
git commit --amend 修改最近一次提交 中(别改已推送的)
git restore f 丢弃工作区改动 ⚠️ 高,改动会永久丢失
git reset --hard HEAD~1 回退到上一次提交 ⚠️⚠️ 极高,会丢提交
git push --force 覆盖远程历史 ⚠️⚠️⚠️ 危险,可能毁了别人的工作

⚠️ 三条铁律:
① 用任何"丢弃/回退"命令前,先 git status 和 git diff 看清楚会丢什么。
② 永远不要对 main 分支 push --force。
③ 拿不准的时候,先 git stash,它把当前改动"暂存到一边",
之后可以 git stash pop 拿回来。这是最安全的"先放一放"。

git stash             # 把当前改动收起来(工作区变干净)
git stash list        # 看看存了什么
git stash pop         # 拿出来(并从 stash 列表删除)

13 调试器:比 print 更强的工具

13.1 先说 print 调试(最常用,别小看)

90% 的问题用 print 就够了。

def mean_of(nums):
    total = 0
    for i, n in enumerate(nums):
        total += n
        print(f"  第{i+1}轮: n={n}, total={total}")   # ← 看中间状态
    return total / len(nums)

输出:

  第1轮: n=1, total=1
  第2轮: n=2, total=3
  第3轮: n=3, total=6
  第4轮: n=4, total=10
均值 = 2.5

💡 print 调试的要点:打印"变量名 = 值",不要只打印值。
print(total) 出来一个 6,哪个变量的 6?
print(f"total={total}") 一眼就清楚。

更好的办法:print(f"{total=}")(Python 3.8+),自动带上变量名。

13.2 什么时候需要真正的调试器

print 够用 需要调试器
单个变量的值 想知道程序"卡在哪一步"
简单循环 深层嵌套 / 递归
逻辑直白 想逐行执行、随时检查所有变量
一次就能看出问题 问题在几十次循环之后的某一次

13.3 breakpoint():最简单的进入方式

Python 3.7+ 只要在代码里插一行:

def mean_of(nums):
    total = 0
    for n in nums:
        breakpoint()        # ← 执行到这里会暂停,进入 pdb
        total += n
    return total / len(nums)

运行后会进入交互式调试(我实跑确认):

> /tmp/dbg2.py(4)mean_of()
-> breakpoint()
(Pdb) p nums
[1, 2, 3, 4]          ← 看变量的值
(Pdb) p n
1
(Pdb) p total
0
(Pdb) n               ← 执行下一行
> /tmp/dbg2.py(5)mean_of()
-> total += n
(Pdb) p total
0
(Pdb) c               ← 继续执行(continue)
均值 = 2.5

13.4 pdb 常用命令

命令 简写 作用
list l 显示当前位置周围的代码
next n 执行下一行(不进入函数)
step s 执行下一行(进入函数)
continue c 继续执行到下一个断点
print x p x 打印变量的值
pp x — 好看地打印(适合大对象)
where w 显示调用栈(我在哪、谁调用的)
up / down u/d 在调用栈上下移动
break N b N 在第 N 行设断点
quit q 退出

⭐ 最常用的三个是 p(看变量)、n(下一步)、c(继续跑)。
掌握这三个,调试效率就翻倍了。

⚠️ 记得调试完删掉 breakpoint()!
忘了删,程序跑到那里会卡住等你输入,你自己会立刻发现,但如果提交了,别人跑你的代码就莫名卡住。
每次提交前搜一下 breakpoint。


14 🔧 技能卡 15:用调试器定位 bug

技能卡 14 教你跑别人的项目;这张卡教你"程序跑起来了但结果不对"时怎么办,这是最难的一类 bug。

14.1 决策一:先用什么工具?

报错了吗?
├── 有报错 → 先读报错(技能卡 1),从最后一行往上读
└── 没报错但结果不对 → 这是"逻辑 bug",最难
        ├── 能猜出大概位置 → print 调试
        └── 猜不出 → 调试器,逐行看

⭐ "没报错但结果不对"比"报错"难十倍,因为没有线索。调试器就是给这类问题制造线索的工具。

14.2 决策二:怎么高效地缩小范围

不要一行一行从头看到尾。用二分法:

步骤 做法
1 找出"最后一个正确的中间结果"和"第一个错误的中间结果"
2 错误就在这两者之间
3 在这段中间插一个 print,重复
records = read_raw(path)          # 这里对吗?print(len(records))
kept, dropped = clean(records)    # 这里对吗?print(len(kept), len(dropped))
report = build_report(kept)       # 到这里才错?

如果 read_raw 就对不上,问题在 IO;
如果 IO 对、clean 之后就错了,问题在清洗规则。
一次砍掉一半代码。

⭐ "二分法定位"是调试的核心策略:
每次把可疑范围缩小一半,几十行代码几轮就定位到了。

14.3 决策三:常见的"结果不对"类型

症状 常见原因 检查什么
循环少跑/多跑一轮 range 边界、< vs <= 打印循环次数
结果差一点点 整除 vs 浮点除(// vs /) 检查每个除法
改了 A 结果 B 也变了 可变对象别名(阶段 7/12) is 检查是不是同一个对象
结果每次不一样 没设随机种子 random.seed(...)
第一次对,第二次不对 可变默认参数 / 类属性 检查定义位置
只有空数据时崩 边界条件没处理 用 []、0、None 试
字符串比较总是不等 多余空格 / 大小写 repr() 看真面目

⚠️ "第一次对,第二次不对"是最阴险的一类,它指向的是可变状态被共享(阶段 7 的可变默认参数、阶段 12 的可变类属性)。
看到这个症状,直接去查"有没有可变的东西定义在了只执行一次的位置"。

14.4 三条新手铁律

① 先复现,再修

不能稳定复现的 bug,你无法确认修好了。
先写一个最小的、必然触发的例子。

② 改之前先想"我预期它输出什么"

"预期 vs 实际"的差距,才是线索。
只盯着实际输出看,你只是在观察,不是在调试。

③ 修完加一个测试

这个 bug 修好了,怎么保证它不回来?
写进 自检.py(技能卡 5)。一次投入,永久防复发。

💡 遇到"怎么都想不通"的 bug:
去走一圈、洗个脸、跟别人讲一遍。
"橡皮鸭调试法"(对着橡皮鸭解释你的代码)真的有效,因为讲出来的过程强迫你把隐含假设显式化,而错误往往藏在隐含假设里。


15 🧠 这跟认知科学有什么关系(编程侧)

概念性内容,无法用代码验证,请对照文献。

分支的"平行世界" ↔ 认知科学里的"反事实思维"(counterfactual thinking)。

15.1 编程侧的事实

Git 分支提供了一种能力:从同一个起点出发,探索不同的可能性,然后选择一条保留。

Git 特性 含义
git branch 从当前状态分叉出一个平行世界
分支之间互不影响 在 feature 上乱改,main 纹丝不动
git merge 合并两条路径
git branch -D 丢弃那个没走通的世界
git log --graph 回看所有走过的路径

15.2 认知科学里的对应物

反事实思维:对"如果当时……会怎样"的想象。

反事实思维类型 例子 Git 类比
上行反事实 "如果我早点开始复习就好了" 看到 feature 分支跑得更好
下行反事实 "还好我没选那条路" 庆幸丢掉了某个分支
加法式 "如果我多做一件事" 加一个新分支试新功能
减法式 "如果我没做那件事" revert 一次提交

研究发现反事实思维有明确的功能:

功能 说明
情绪调节 下行反事实能缓解后悔("还好……")
行为调节 上行反事实能促进下次改进("下次要……")
因果推断 "如果没做 X,结果会不同吗"是因果判断的核心

⭐ 最后一条最深刻:
因果推断依赖"反事实"。"X 导致了 Y"的意思是
"如果 X 没发生,Y 就不会发生"。
而"如果 X 没发生"是一个你无法观测的世界,你只能想象它,或者……在计算机里真的跑一遍。

这就是"因果推断的计算化":有了可执行的模型,
反事实从"想象"变成了"可以实际执行的分支"。

调试器 ↔ 元认知监控(metacognitive monitoring)

调试的本质是一种元认知活动:

调试 元认知
形成假设("问题应该在这段") 元认知判断
设计检验(插 print 看变量) 监控策略
"预期 vs 实际"的比较 监控的核心机制
发现假设错了,换方向 监控失败后的策略调整

"我预期它输出什么",这一步正是元认知监控在起作用。
新手调试效率低,往往是缺少"先写下预期"这一步。
没有预期,就没有"差距",也就没有线索。

⚠️ 诚实说明:
- 反事实思维是认知心理学的重要研究主题,有大量实验工作
(Kahneman & Miller 1986 的"norm theory"、Roese 1997 的综述、
以及反事实与因果推断的关系研究,如 Pearl 的因果图模型)。
- 但"反事实思维的确切机制""它与因果判断的先后关系"仍有争论。
- Git 分支与反事实的类比是教学工具,不是认知机制的主张。
大脑里没有"分支",人也不能真的执行"如果……"。
请以文献为准。


16 编程作业 S15

第 3 题参考解法:

# 制造冲突
git switch -c other
echo "other版本" > f.txt
git add f.txt && git commit -m "other改了同一行"

git switch main
echo "main版本" > f.txt
git add f.txt && git commit -m "main也改了同一行"

git merge other
# CONFLICT (content): Merge conflict in f.txt

cat f.txt
# <<<<<<< HEAD
# main版本
# =======
# other版本
# >>>>>>> other

解决:手工编辑 f.txt,改成你想要的内容,删掉三个标记行

# 编辑后:
git add f.txt
git commit -m "解决冲突:合并 other"

三个标记的含义:

标记 含义
<<<<<<< HEAD 你当前分支(main)的版本从这里开始
======= 两个版本的分界线
>>>>>>> other 被合并分支(other)的版本到这里结束

⚠️ 三个标记行必须全部删掉。
它们是 Git 加的临时标记,不是代码,留着会直接报语法错误。

第 9 题答案:

def add(x, lst=[]):       # ⚠️ 默认值 [] 只在定义时创建一次
    lst.append(x)
    return lst


print(add(1))    # [1]
print(add(2))    # [1, 2]   ← 不是 [2]!默认列表被复用了
print(add(3))    # [1, 2, 3]

原因:默认参数的值只在函数定义时求值一次,
之后每次调用共用同一个列表对象。

修正:

def add(x, lst=None):
    if lst is None:
        lst = []
    lst.append(x)
    return lst


print(add(1))    # [1]
print(add(2))    # [2]     ✓

这是阶段 5 的坑,在这阶段再出现一次,因为它太容易犯了。
规律(第三次强调):可变对象不能出现在"只执行一次"的位置。
这类位置有三处:默认参数、类属性、模块级变量。

"第一次对,第二次不对"就是这个坑的典型症状,看到这个症状直接往这查。

第 10 题参考做法:

# ① 故意引入 bug:把 stats_utils.py 里的
return sum(nums) / len(nums)
# 改成
return sum(nums) // len(nums)      # 整除,结果会截断

# ② 二分法定位:
#    - 先 print(sum(nums))       -> 对
#    - 再 print(len(nums))       -> 对
#    - 那问题就在 "/" 这一行     -> 定位到

# ③ 加测试防复发(写进 自检.py):
from stats_utils import mean_of

assert abs(mean_of([1, 2, 3]) - 2.0) < 1e-9, f"均值错了:{mean_of([1,2,3])}"
assert abs(mean_of([0.5, 0.25]) - 0.375) < 1e-9, "小数均值错了"
print("均值检查通过")

💡 mean_of([1,2,3]) 用 // 恰好也是 2,测不出来!
必须用能暴露问题的用例:[0.5, 0.25] 用 // 会得到 0,
而正确值是 0.375。这就是"检查用例要挑边界"的意义(技能卡 5)。


本阶段自评

  1. 数学:相似矩阵的定义是什么?相似的两个矩阵有哪些共同的不变量?
  2. 数学:对角化的条件是什么?"几何重数 < 代数重数"意味着什么?
  3. 数学:$P$ 和 $\Lambda$ 分别由什么构成?列顺序为什么必须对应?
  4. 数学:$A^k = ?$ 为什么这个公式让计算变简单了?
  5. 数学:Fibonacci 矩阵的特征值和黄金比例是什么关系?
  6. 数学:实对称矩阵有什么特殊性质?为什么给计算带来好处?
  7. 数学:什么时候该用 eig,什么时候该用 eigh?用错了会怎样?
  8. 数学:最大的特征值决定了变换的什么?三种情况(>1、=1、<1)分别对应什么长期行为?
  9. 编程:Git 分支的本质是什么(它其实是个什么东西)?
  10. 编程:合并冲突时,那三个标记行分别是什么意思?解决后必须做什么?
  11. 编程:git restore、git restore --staged、git reset --hard 哪个最危险?为什么?
  12. 编程:git fetch 和 git pull 的区别是什么?
  13. 编程:breakpoint() 和 print 调试各自适合什么场景?
  14. 技能:调试的"二分法"是什么?为什么它有效?
  15. 技能:"第一次对,第二次不对"这个症状指向什么原因?
  16. 连接:为什么说"因果推断依赖反事实"?Git 分支和反事实有什么深层联系?

全部答得上 → 进阶段 16,最后一段。
有任何一个答不上 → 回去补,别跳。

最后更新:2026-09-26