阶段 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
- [ ] 1. 求 $\begin{pmatrix}3&0\\0&5\end{pmatrix}$ 的对角化(想想:还需要算吗?)
- [ ] 2. 判断 $\begin{pmatrix}1&2\\2&1\end{pmatrix}$ 能否对角化,若能,写出 $P$ 和 $\Lambda$
- [ ] 3. 把 $\begin{pmatrix}2&1\\1&2\end{pmatrix}$ 对角化,并验算 $P\Lambda P^{-1}=A$
- [ ] 4. 用对角化求 $\begin{pmatrix}2&1\\1&2\end{pmatrix}^4$,再用直接乘检验
- [ ] 5. 求 $\begin{pmatrix}1&1\\1&0\end{pmatrix}^{10}$(Fibonacci 矩阵),并说出它对应的 Fibonacci 数
- [ ] 6. 说明 $\begin{pmatrix}2&0\\0&2\end{pmatrix}$ 和 $\begin{pmatrix}2&1\\0&2\end{pmatrix}$ 特征多项式相同但不相似的理由
- [ ] 7.(重点)验证 $\begin{pmatrix}2&1\\1&2\end{pmatrix}$ 的正交对角化 $Q\Lambda Q^T$,并验证 $Q^TQ=I$
- [ ] 8. 为什么实对称矩阵可以正交对角化?这给计算带来什么好处?
- [ ] 9. 已知 $A$ 的特征值是 $0.5$ 和 $0.8$,$A^k$ 当 $k\to\infty$ 时会怎样?
- [ ] 10.(Python)用 numpy 验证作业 4–5,并求一个马尔可夫链的稳态分布
第 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(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
- [ ] 1. 在阶段 14 的项目里建一个分支
try-feature,改点东西,再切回 main,确认 main 没变 - [ ] 2. 把
try-feature合并到 main,确认合并成功 - [ ] 3.(重点)制造一次冲突(两个分支改同一行),手工解决它,说清三个标记的含义
- [ ] 4. 解释
git fetch和git pull的区别,什么情况下该先fetch - [ ] 5. 说明
git restore f和git restore --staged f的区别,哪个会丢工作 - [ ] 6. 用
git stash把当前改动收起来,再git stash pop拿回来 - [ ] 7. 写一个故意算错均值的函数,用
print调试找出问题 - [ ] 8.(重点)同一个 bug 用
breakpoint()+ pdb 再找一遍,说出p、n、c三个命令的作用 - [ ] 9.(技能)下面代码"第一次调用对,第二次不对",找出原因:
def add(x, lst=[]): lst.append(x) return lst- [ ] 10.(技能)给项目里的
mean_of故意引入一个 bug(比如//代替/),用二分法定位到它,并写进自检.py - [ ] 11.(Python)在 GitHub 上建一个仓库,把阶段 14 的项目推上去(注意
.gitignore先写好)
- [ ] 10.(技能)给项目里的
第 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)。
本阶段自评
- 数学:相似矩阵的定义是什么?相似的两个矩阵有哪些共同的不变量?
- 数学:对角化的条件是什么?"几何重数 < 代数重数"意味着什么?
- 数学:$P$ 和 $\Lambda$ 分别由什么构成?列顺序为什么必须对应?
- 数学:$A^k = ?$ 为什么这个公式让计算变简单了?
- 数学:Fibonacci 矩阵的特征值和黄金比例是什么关系?
- 数学:实对称矩阵有什么特殊性质?为什么给计算带来好处?
- 数学:什么时候该用
eig,什么时候该用eigh?用错了会怎样? - 数学:最大的特征值决定了变换的什么?三种情况(>1、=1、<1)分别对应什么长期行为?
- 编程:Git 分支的本质是什么(它其实是个什么东西)?
- 编程:合并冲突时,那三个标记行分别是什么意思?解决后必须做什么?
- 编程:
git restore、git restore --staged、git reset --hard哪个最危险?为什么? - 编程:
git fetch和git pull的区别是什么? - 编程:
breakpoint()和print调试各自适合什么场景? - 技能:调试的"二分法"是什么?为什么它有效?
- 技能:"第一次对,第二次不对"这个症状指向什么原因?
- 连接:为什么说"因果推断依赖反事实"?Git 分支和反事实有什么深层联系?
全部答得上 → 进阶段 16,最后一段。
有任何一个答不上 → 回去补,别跳。