▤书架 ← 返回索引
▶目录

阶段 16 讲义

数学 11h:线代复习 · SVD 启蒙
编程 9h:环境管理与阶段总结


第一部分 · 数学

这是大一上最后一个数学阶段。做两件事:把线性代数串成一条线,然后认识 SVD。

SVD(奇异值分解)是现代数据科学的支柱:图像压缩、推荐系统、降维、去噪、伪逆……
全都建立在它上面。这一节只做"启蒙",让你知道它是什么、为什么重要。

1 线性代数全景回顾

阶段 8–10、13、15 学的其实是同一个故事的不同章节。

1.1 一条主线:解 $Ax = b$

    行列式(阶段 8)          矩阵与逆(阶段 9)
    判断"有没有唯一解"   →    把"除法"做出来
            │                        │
            └────────────┬───────────┘
                         ▼
              向量组与秩(阶段 10)
        回答"到底有几个解"——秩决定一切
                         │
                         ▼
              特征值(阶段 13、15)
        "哪些方向不被扭转" + 对角化
                         │
                         ▼
                    SVD(阶段 16)
        "任何矩阵都能拆成 旋转×拉伸×旋转"

1.2 核心概念对照表

概念 一句话 阶段
行列式 一个数,衡量"体积缩放"和"是否可逆" 08
逆矩阵 矩阵里的除法,$AA^{-1}=I$ 09
秩 独立的行/列数,决定解的个数 10
线性相关 有人是多余的 10
特征值 沿某方向只拉伸的倍数 13
对角化 换基让矩阵变简单:$A=P\Lambda P^{-1}$ 15
正交矩阵 $Q^TQ=I$,求逆就是转置 15
奇异值 任何矩阵的"拉伸倍数"(特征值的推广) 16
SVD 任何矩阵都能拆成三个好矩阵的乘积 16

关键的关系串联:

$$\underbrace{\det A = \prod\lambda_i}_{\text{行列式}} \qquad \underbrace{\text{rank} = \text{非零特征值个数}}_{\text{秩}} \qquad \underbrace{A = Q\Lambda Q^T}_{\text{对称矩阵}} \qquad \underbrace{A = U\Sigma V^T}_{\text{任何矩阵}}$$

最后一个是前面所有的推广。

2 SVD 是什么

任何矩阵(不要求方阵、不要求对称、不要求可逆)都能拆成:

$$\boxed{A = U\Sigma V^T}$$

部分 形状 性质 含义
$U$ $m\times m$ 正交($U^TU=I$) 输出空间的旋转
$\Sigma$ $m\times n$ 对角,非负且降序 拉伸倍数 = 奇异值
$V^T$ $n\times n$ 正交($V^TV=I$) 输入空间的旋转

奇异值 $\sigma_1\ge\sigma_2\ge\cdots\ge0$,是 $\Sigma$ 的对角线元素。

⭐ SVD 说的是:任何一个线性变换,不管多复杂,
都等价于"先旋转 → 再沿坐标轴拉伸 → 再旋转"。

这就是它能成为"线性代数终极定理"的原因,它没有任何前提条件。

2.1 完整例子

$$A = \begin{pmatrix}3&1\\1&3\\0&0\end{pmatrix} \quad (3\times2)$$

numpy 实算:

$$U = \begin{pmatrix}-0.707&-0.707&0\\-0.707&0.707&0\\0&0&1\end{pmatrix}, \quad \Sigma = \begin{pmatrix}4&0\\0&2\\0&0\end{pmatrix}, \quad V^T = \begin{pmatrix}-0.707&-0.707\\-0.707&0.707\end{pmatrix}$$

奇异值:$\sigma_1 = 4,\ \sigma_2 = 2$

验证重构:

$$U\Sigma V^T = \begin{pmatrix}3&1\\1&3\\0&0\end{pmatrix} = A \quad ✓ \text{(实算确认)}$$

验证正交性:$U^TU = I_3$ ✓,$VV^T = I_2$ ✓(实算确认)

⚠️ 注意 $U$ 和 $V$ 的维度:$A$ 是 $3\times2$,
所以 $U$ 是 $3\times3$、$V$ 是 $2\times2$,它们不是同一个大小。
重构时要用 $U$ 的前 2 列(对应 2 个非零奇异值)。

3 SVD 和特征值的关系

这是最容易被绕晕的地方,但说起来很简单:

$$\boxed{A^T A \text{ 的特征值} = \text{奇异值的平方}}$$

具体地:

$$A^TA\,v_i = \sigma_i^2\,v_i \quad\Longrightarrow\quad v_i \text{ 是 } V \text{ 的列(右奇异向量)}$$

$$AA^T\,u_i = \sigma_i^2\,u_i \quad\Longrightarrow\quad u_i \text{ 是 } U \text{ 的列(左奇异向量)}$$

验证(实算):

$$A^TA = \begin{pmatrix}10&6\\6&10\end{pmatrix}, \quad \text{特征值} = 4,\ 16$$

$$\sigma^2 = 2^2,\ 4^2 = 4,\ 16 \quad ✓ \text{完全一致}$$

💡 所以 SVD 并没有引入全新的数学。
它可以"用特征值算出来":构造 $A^TA$(这是对称矩阵!),
求它的特征值和特征向量,开根号就得到奇异值。

但实际计算不会这么做,因为 $A^TA$ 的条件数是 $A$ 的平方,
数值上会损失一半精度。实际用的是专门的 SVD 算法(Golub-Kahan 等)。

3.1 对称矩阵的特殊情形

如果 $A$ 是对称矩阵,那 SVD 和特征分解就几乎一样了:

$$\sigma_i = |\lambda_i|$$

实算验证:

矩阵 特征值 奇异值
$\begin{pmatrix}4&2\\2&3\end{pmatrix}$ $1.438,\ 5.562$ $5.562,\ 1.438$ ✓
$\begin{pmatrix}4&2\\2&-3\end{pmatrix}$ $\mathbf{-3.531},\ 4.531$ $4.531,\ \mathbf{3.531}$ ✓

⚠️ 看第二行:特征值有负数,但奇异值永远是正的(取绝对值)。
这就是 SVD 比特征分解适用范围更广的原因。
特征值可以是复数、可以没有、可以不存在(不可对角化),
但奇异值永远存在、永远是实数、永远非负。

4 应用一:低秩近似(数据压缩的原理)

这是 SVD 最重要的应用。

核心事实(Eckart–Young 定理):只用前 $k$ 个奇异值,就能得到"最好的"近似:

$$A \approx U_k\Sigma_kV_k^T$$

而且这是所有秩为 $k$ 的矩阵里,误差最小的那一个(在 Frobenius 范数下)。

实算给你看:我造了一个 $10\times5$ 的接近低秩的矩阵:

奇异值 $145.533,\ 2.134,\ 1.904,\ 1.313,\ 0.718$

注意第一个奇异值远大于其他,说明这个矩阵几乎是秩 1 的。

保留前 $k$ 个 相对误差 需要的数
$k=1$ $0.0222$ $16$
$k=2$ $0.0166$ $32$
$k=3$ $0.0103$ $48$
全部($k=5$) $0$ $50$

看第一行:只用 16 个数就还原了原矩阵(50 个数),误差只有 2%。

⭐ 这就是图像压缩的原理:
一张图片是个矩阵,它的奇异值衰减很快,
只存前几十个奇异值和对应的向量,就能还原出肉眼看不出差别的图片。
JPEG、推荐系统、潜在语义分析,用的都是这件事。

💡 同时这也解释了 PCA:
对中心化的数据矩阵做 SVD,右奇异向量就是主成分方向,
奇异值的平方 ÷(n−1) 就是特征值(= 各主成分的方差)。
PCA 就是"对数据做 SVD 然后截断"。

5 应用二:伪逆(解决"不能求逆"的问题)

回忆阶段 9:只有方阵、且 $\det\ne0$ 才有逆矩阵。

但现实里的矩阵大多不是方阵(比如 $X$ 是 $n\times p$,被试数 $\ne$ 变量数)。

SVD 给出替代品:伪逆(Moore-Penrose 伪逆)。

$$A^+ = V\Sigma^+U^T$$

其中 $\Sigma^+$ 是"把非零奇异值取倒数,再转置"。

它解决的是最小二乘问题 $Ax\approx b$。

$$\hat{x} = A^+b$$

⭐ 这正是线性回归的解法:
$\hat\beta = (X^TX)^{-1}X^Ty$ 在 $X^TX$ 不可逆时会崩,
但用伪逆(np.linalg.pinv 底层就是 SVD)总能给出一个答案,
它给的是"最小范数最小二乘解"。

阶段 8/9/10 反复遇到的"共线性导致求逆失败",SVD 是最终的工程解法。

6 常见的坑

坑 说明
以为 SVD 要求方阵 任何 $m\times n$ 矩阵都有 SVD,这正是它的威力
奇异值当特征值用 奇异值 $\ge0$,特征值可以是负的、复的
$U$ 和 $V$ 维度搞混 $U$ 是 $m\times m$,$V$ 是 $n\times n$,大小不同
重构时没截断 $A = U\Sigma V^T$ 里,$\Sigma$ 的形状要和 $U$、$V$ 的列数匹配
以为奇异值排序无所谓 numpy 返回时已按降序排好,这个顺序就是重要性顺序
用 A.T@A 的特征值算奇异值 数学上对,数值上会损失精度,实际用 svd
以为"低秩近似"是随便截断 Eckart–Young 定理保证它是最优的
符号约定 $U$、$V$ 的列符号可以整体翻转(不影响重构),不同库可能不同

7 线性代数到此为止

学完这 9 个阶段,你手里有这些能力。

能力 具体
判断解的存在性 用秩,不用解方程
解线性方程组 消元、逆矩阵、伪逆
理解线性变换 拉伸、旋转、投影
找"不变的量" 特征值、行列式、迹、秩
分解矩阵 对角化、正交对角化、SVD
知道什么时候"不能算" 奇异、病态、共线性

⭐ 这些是大二所有量化课程的地基:
多元统计、数值分析、机器学习、心理测量、脑影像分析,
没有一个是能绕开线性代数的。


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

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

表征相似性分析(RSA)和 MDS = SVD 在认知神经科学里最直接的两次出场。

8.1 RSA 是怎么做的

表征相似性分析(Representational Similarity Analysis) 是近十几年认知神经科学最流行的方法之一。

核心想法:

不要问"这个神经元对哪个刺激反应强",而要问"哪些刺激在这个区域里更相似"。

步骤:

步 做什么 数学
1 每种刺激得到一组神经活动模式 向量
2 两两算距离,得到 RDM(表征不相似矩阵) 距离矩阵
3 比较不同脑区/不同模型的 RDM 相关系数
4 (可选)把 RDM 投影到低维看结构 MDS = 特征分解

8.2 实算给你看

我造了 5 个刺激在 3 个"神经元"上的活动模式:

$$P = \begin{pmatrix}1.0&0.5&0.0\\0.9&0.6&0.1\\0.0&0.0&1.0\\0.1&0.1&0.9\\0.5&0.5&0.5\end{pmatrix}$$

算两两之间的欧氏距离,得到 RDM:

$$\text{RDM} = \begin{pmatrix}0&0.173&1.5&1.334&0.707\\0.173&0&1.407&1.237&0.574\\1.5&1.407&0&0.173&0.866\\1.334&1.237&0.173&0&0.693\\0.707&0.574&0.866&0.693&0\end{pmatrix}$$

看第一二行:刺激 1 和刺激 2 的距离是 $0.173$(很近),刺激 3 和 4 也是 $0.173$。
说明它们分别构成了两组。

8.3 MDS:把 RDM 变回坐标(这一步用特征分解)

经典 MDS 的做法:

  1. 双中心化:$B = -\frac12 J(\text{RDM}^2)J$,其中 $J = I - \frac1n\mathbf{1}\mathbf{1}^T$
  2. 对 $B$ 做特征分解:$B = V\Lambda V^T$
  3. 取前 $k$ 个正特征值,坐标 = $V_k\sqrt{\Lambda_k}$

实算结果($B$ 的特征值):

$$-0,\ -0,\ 0,\ 0.039,\ \mathbf{1.893}$$

只有 2 个是正的,说明这 5 个刺激本质上活在 2 维里(虽然原始数据是 3 维)。

MDS 二维坐标:

刺激 坐标
1 $(-0.716,\ 0.113)$
2 $(-0.622,\ -0.033)$
3 $(0.783,\ 0.055)$
4 $(0.614,\ 0.014)$
5 $(-0.059,\ -0.149)$

1、2 在左边,3、4 在右边,和 RDM 的结构对应。
前两个特征值解释了 100% 的(正)变异(实算确认)。
用这 2 维坐标重构距离,最大误差为 $0.0$,完美还原(实算确认)。

⭐ 所以 MDS 和 PCA 和 SVD 是同一件事的三种说法:
- 对数据矩阵做 SVD → PCA
- 对距离矩阵做特征分解 → 经典 MDS
- $A$ 的右奇异向量 = $A^TA$ 的特征向量 = PCA 的主成分方向

8.4 为什么 RSA 在认知科学里这么重要

优势 说明
不受"单位"影响 不同脑区、不同测量方式的活动强度不可比,但距离结构可比
能做模型比较 把"行为模型""计算模型"的预测也变成 RDM,看哪个和脑区最像
能跨模态 fMRI、EEG、MEG、单细胞记录,全都能算 RDM
能比较物种 猴子的 RDM 和人的 RDM 可以直接比

典型应用:

研究问题 怎么做
视觉皮层怎么表征物体 算物体之间的 RDM,看是否符合某个 CNN 层的 RDM
语言理解的脑区分工 比较句子/词的 RDM
记忆的神经表征 比较"学过的"和"新"刺激的 RDM
个体差异 比较不同被试的 RDM 相似度

⚠️ 诚实说明:
- 上面的 RDM、双中心化特征值、MDS 坐标、重构误差都是我实算的(可复现),
"经典 MDS = 双中心化 + 特征分解""SVD 与 PCA 的关系"是数学事实。
- 但 RSA 的统计推断怎么做(怎么检验"两个 RDM 真的相关吗")
是一个活跃的方法学领域,早期做法有不少批评,
近年有置换检验、噪声上限校正等更严格的方法。
- MDS 有经典 MDS 和非度量 MDS 等多种变体,本讲义只演示了经典 MDS。
- 请以 Kriegeskorte et al. (2008, Frontiers in Systems Neuroscience)
和 Representational Similarity Analysis 方向的后续文献为准。
我保证数学,不保证统计推断的做法。


9 数学作业 S16

第 10 题参考代码:

import numpy as np

np.set_printoptions(precision=4, suppress=True)


def svd_demo(A, name=""):
    """完整演示 SVD 的各个性质。"""
    A = np.asarray(A, dtype=float)
    m, n = A.shape
    U, S, Vt = np.linalg.svd(A)

    print(f"=== {name or A.tolist()} ===")
    print(f"A 的形状: {m}×{n}")
    print(f"U 形状 {U.shape}, 奇异值 {len(S)} 个, V^T 形状 {Vt.shape}")
    print("奇异值:", S)

    # ① 重构(注意 U 要取前 k 列,k = 奇异值个数)
    k = len(S)
    recon = U[:, :k] @ np.diag(S) @ Vt
    print("重构正确?", np.allclose(A, recon))

    # ② 正交性
    print("U 正交?", np.allclose(U.T @ U, np.eye(m)))
    print("V 正交?", np.allclose(Vt @ Vt.T, np.eye(n)))

    # ③ 与特征值的关系:A^T A 的特征值 = 奇异值²
    evals = np.linalg.eigvalsh(A.T @ A)
    print("A^T A 特征值      :", np.sort(evals))
    print("奇异值的平方      :", np.sort(S ** 2))
    print("两者一致?", np.allclose(np.sort(evals), np.sort(S ** 2)))

    # ④ 低秩近似
    if min(m, n) > 1:
        print("低秩近似(相对误差):")
        for kk in range(1, min(m, n) + 1):
            Ak = U[:, :kk] @ np.diag(S[:kk]) @ Vt[:kk]
            err = np.linalg.norm(A - Ak) / np.linalg.norm(A)
            print(f"   前 {kk} 个奇异值: {err:.6f}")
    print()


svd_demo([[3, 1], [1, 3], [0, 0]], "3×2 矩形矩阵")
svd_demo([[2, 1], [1, 2]], "对称矩阵")
svd_demo([[4, 2], [2, -3]], "有负特征值的对称矩阵")

print("=== 伪逆 vs 普通逆 ===")
X = np.array([[1., 2.], [2., 4.], [3., 6.]])   # 列线性相关(第2列=第1列×2)
print("X =\n", X)
print("X^T X 行列式 =", round(np.linalg.det(X.T @ X), 10), " ← 0,不可逆!")

try:
    np.linalg.inv(X.T @ X)
    print("求逆成功?")
except np.linalg.LinAlgError as e:
    print("np.linalg.inv 失败:", e)

pinv = np.linalg.pinv(X)
print("伪逆形状:", pinv.shape)
print("伪逆能算出来 ✓(这就是共线性时的兜底方案)")

第二部分 · 编程

10 为什么需要环境管理

先看一个真实的灾难:

# 三个月前的你
pip install numpy pandas matplotlib

# 现在的你(跑同一个脚本)
ImportError: cannot import name 'foo' from 'numpy'
# 或者更糟:能跑,但结果和之前不一样

问题出在哪:

问题 后果
装的包越来越多 版本互相冲突,没人知道哪个包依赖哪个
升级了某个包 三个月前的代码跑不动了
换电脑 / 别人跑 "在我电脑上能跑",经典的不可复现
一个项目要 numpy 1.x,另一个要 2.x 无法共存

环境管理解决的就是这件事:给每个项目一套独立的、版本明确的包。

11 三层的"环境"概念

先分清楚三个东西:

层级 是什么 怎么看
Python 解释器 哪个 Python 在跑 which python / sys.executable
环境 一套独立的包安装目录 虚拟环境 / conda 环境
包 具体的库和版本 pip list / conda list

你现在的情况(本项目用的):

~/miniconda3/bin/python     ← 解释器
~/miniconda3/envs/...       ← conda 环境(base 是默认的)
numpy 2.5.3, pandas 3.0.5, matplotlib 3.11.1, scipy 1.18.1, sympy 1.14.0

检查你自己环境的命令:

~/miniconda3/bin/python -c "import sys; print(sys.executable)"
~/miniconda3/bin/python -c "import numpy; print(numpy.__version__)"
~/miniconda3/bin/python -m pip list

12 venv:Python 自带的虚拟环境

venv 是标准库自带的(不用装任何东西),最轻量。

# ① 在项目目录创建虚拟环境(会生成 .venv/ 目录)
python -m venv .venv

# ② 激活它
source .venv/bin/activate          # macOS / Linux
# .venv\Scripts\activate           # Windows(PowerShell)

# ③ 激活后,python 和 pip 都指向这个环境了
which python                       # → .../.venv/bin/python
pip install numpy pandas

# ④ 用完退出
deactivate

我实跑验证过:

$ .venv/bin/python -c "import sys; print(sys.executable)"
/private/tmp/envtest/.venv/bin/python      ← 指向虚拟环境 ✓

$ ~/miniconda3/bin/python -c "import sys; print(sys.executable)"
/Users/mamingyu/miniconda3/bin/python      ← 系统环境,不受影响 ✓

⭐ 核心理解:虚拟环境就是"一套独立的包安装目录"。
激活的本质是改 PATH,让 python 指向那一套。

12.1 venv vs conda

venv conda
来源 Python 自带 需安装 miniconda/anaconda
管什么 只管 Python 包 还能管非 Python 依赖(如 C 库、R)
速度 快、轻量 慢一些
适合 纯 Python 项目 科学计算、需要底层库

你现在用 conda(因为已经装了 miniconda),两个都行。建议:

12.2 conda 常用命令(你用得上)

conda create -n myenv python=3.11     # 建环境
conda activate myenv                  # 激活
conda deactivate                      # 退出
conda env list                        # 列出所有环境
conda install numpy pandas            # 装包
conda env export > environment.yml    # 导出环境(复现用)
conda env create -f environment.yml   # 从文件重建环境

13 requirements.txt:让别人能复现你的环境

这是项目的"依赖清单"。

# 导出当前环境的所有包
pip freeze > requirements.txt

# 别人(或者三个月后的你)一键装齐
pip install -r requirements.txt

更好的写法:只写直接依赖,不要 pip freeze 的全量列表

numpy>=2.0
pandas>=2.2
matplotlib>=3.8
scipy>=1.11

两种写法的对比:

写法 优点 缺点
pip freeze 全量 完全可复现 几百行,含所有间接依赖,难读
手写直接依赖 可读、好维护 依赖解析可能给出不同版本

💡 实践建议:
开发时用手写清单(好读),要严格复现时用全量 pip freeze。
或者用 pip-tools / poetry / uv 这类工具管理,不过大一上你还不需要。

给阶段 14 的项目加上依赖清单:

cd ~/Desktop/大一/编程/项目-反应时分析

建一个 requirements.txt:

# 这个项目只用了标准库,所以没有第三方依赖。
# 但显式写出来,是为了说明"依赖清单"这件事本身。
#
# 如果以后用到了 numpy:
# numpy>=2.0

⭐ 注意阶段 14 的项目其实只用标准库,
这本身就是个好设计:依赖越少,越容易在别人电脑上跑起来。
"零依赖"是最强的可复现性。

14 换电脑 / 换电脑后的完整复现流程

这是技能卡 16 的主题,这里先给流程。

场景:你从宿舍的电脑换到图书馆的电脑(或者买了新电脑),想把项目跑起来。

步 做什么 命令
1 拿到代码 git clone <url>
2 确认 Python 版本 cat .python-version 或看 README
3 建独立环境 python -m venv .venv
4 激活 source .venv/bin/activate
5 装依赖 pip install -r requirements.txt
6 跑测试 python 自检.py ← 先跑测试,不要先跑主程序
7 跑主程序 python main.py

⭐ 第 6 步的顺序很重要:
先跑测试,能立刻知道"是环境问题还是数据问题"。
测试过了说明环境没问题,那主程序出问题就是数据或参数的问题。

15 大一上阶段总结

16 个阶段走完了。盘点一下你拿到了什么。

15.1 数学

模块 掌握
极限 ε-N、ε-δ、等价无穷小、洛必达
微分 定义、链式法则、中值定理、凹凸性、作图
积分 换元、分部、定积分、面积/体积/平均值、反常积分
线性代数 行列式、矩阵运算、逆、秩、方程组、向量空间、特征值、对角化、SVD

15.2 编程

模块 掌握
语言基础 类型、可变性、运算符、条件、循环
数据结构 列表、字典、集合、元组、字符串
函数 参数、作用域、返回值、可变默认参数
面向对象 类、实例、继承、魔术方法、property、组合
文件与异常 读写、路径、异常处理
模块化 拆文件、import、__main__
工具 Git(提交/分支/合并/远程)、调试器
环境 venv/conda、requirements.txt、可复现

15.3 张技能卡(这可能是最实用的收获)

# 技能
01 读懂报错信息
02 自己找答案(help/dir)
03 循环出错怎么查
04 函数调试 + assert
05 给函数写检查用例
06 读懂别人的函数签名
07 画内存图,理解"可变"
08 数据结构怎么选
09 字符串怎么切、怎么格式化
10 文件读写与路径
11 拆分代码成多个文件
12 类与实例的关系
13 Git 基本工作流
14 从零把一个项目跑起来
15 用调试器定位 bug
16 换电脑怎么复现环境

15.4 你现在的实际能力

能独立完成的:

这正是《学习路线》里"阶段 16 末"定的目标的一部分:

GPA 稳住 + 1 个 GitHub 项目 + Python 语法全通 + 高数上/线代基础扎实

16 接下来做什么

《学习路线》的规划是:

时间 数学 编程
大一下 高数(下):多元微积分、重积分、级数 | 概率论 numpy/pandas/matplotlib | 第一个数据分析项目 | 英语攻坚(雅思)
大二上 数理统计、离散数学、常微分方程 工程能力(测试/环境/命令行)| PsychoPy | 进实验室
大二下 多元统计、数值分析 面向对象进阶 | PyTorch 入门

但有几件事比"进入下一阶段"更紧急:

优先级 做什么 为什么
⭐⭐⭐ 把阶段 01–16 的作业补完 跳过作业的进度是假的
⭐⭐⭐ 把阶段 14 的项目推上 GitHub 这是你简历的第一块砖
⭐⭐ 把编程 4 篇笔记 + 2 份速查表过一遍 建立你自己的检索体系
⭐⭐ 英语按计划推进 有截止日的东西不能拖
⭐ 复习 16 份讲义的自评问题 答不上就回去补

⭐ 进度可以慢,但不能跳过作业。
你已经走完了大一上最难的 16 个阶段,这本身就不容易。
接下来是把它变成"真的会",不能停在"看起来完成了"。


17 🔧 技能卡 16:换电脑怎么复现环境

技能卡 15 教你修 bug;这张卡教你"在另一台电脑上,从零把这个项目跑起来"。这是可复现性的最后一环,也是你以后发给导师或合作者的第一件事。

17.1 为什么会"在我电脑上能跑"

你的电脑上有一堆你没意识到的东西:

隐含依赖 举例
Python 版本 3.11 和 3.13 行为可能不同
第三方包版本 numpy 1.x 和 2.x 有 breaking change
全局装的包 你三个月前 pip install 过,自己忘了
环境变量 PYTHONPATH、PATH 被改过
操作系统差异 路径分隔符、换行符

"在我电脑上能跑"是软件工程里最经典的借口,也是最该被消除的。

17.2 五个复现要素(缺一不可)

# 要素 文件/做法
1 代码 Git 仓库
2 Python 版本 README 写明,或 .python-version
3 依赖清单 requirements.txt 或 environment.yml
4 数据 放进 data/,或用脚本下载(大文件别进 Git)
5 操作说明 README 里写清楚"怎么跑"

⭐ 判断标准:把仓库发给一个陌生人,他能不能跑出和你一样的结果?
能,就叫"可复现"。

17.3 完整流程(存下来,以后照着做)

在"发送方"(你的电脑):

cd 项目目录
git status                      # ① 确认要提交的都提交了
cat .gitignore                  # ② 确认该忽略的忽略了
# ③ 记录 Python 版本
python --version > .python-version
# ④ 生成依赖清单
pip freeze > requirements.txt
# ⑤ 推送
git add . && git commit -m "整理依赖清单和环境说明"
git push

在"接收方"(另一台电脑):

git clone <仓库地址>
cd 项目目录

python -m venv .venv            # ① 建独立环境
source .venv/bin/activate       # ② 激活
pip install -r requirements.txt # ③ 装依赖

python 自检.py                   # ④ 先跑测试!
python main.py                  # ⑤ 再跑主程序

17.4 README 必须写清楚的四件事

① 环境要求,写清 Python 版本和依赖

## 环境要求
- Python 3.11+
- 依赖见 requirements.txt

② 怎么跑,命令逐步列出

python -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt
python 自检.py     # 先跑测试确认环境
python main.py     # 跑主流程

③ 数据从哪来,文件在哪、什么格式

## 数据从哪来
data/raw.csv,格式见下方说明。

④ 已知问题。这一节最容易被省略,但值得写

## 已知问题
- 剔除阈值是我自己定的,不同阈值结果会变
- 还没处理缺失值

⚠️ "已知问题"这一节防止别人(和三个月后的你)误以为"结果就是这样"。

17.5 三条新手铁律

① .venv/ 永远不要提交

写进 .gitignore(阶段 13 讲过)。
它是几百 MB 的本地文件,换电脑后完全没用,别人自己重建就行。

② 依赖清单要么精确,要么宽松,但不要说谎

numpy(不写版本)意味着"任何版本都行",但可能不是真的。
如果代码对版本敏感,必须写 numpy>=2.0,<3。

③ 先跑测试,再跑主程序

自检.py 过了 → 环境没问题 → 主程序出问题一定是数据/参数问题。
这一步能省你大量排查时间。

💡 遇到"别人跑不了你的代码":
让他把完整报错发给你,然后问三个问题:
1. python -V 是什么?
2. pip list 里有没有你用的包?
3. 是不是从项目根目录运行的?

这三个问题能解决 90% 的"跑不了"。


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

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

环境管理 ↔ 认知科学里的"语境依赖性"和"编码特异性"。记忆不是独立于环境的。

18.1 编程侧的事实

"在我电脑上能跑"是一个真实的现象:同一份代码,在不同环境下行为不同。

环境因素 影响
Python 版本 语法/行为差异
包版本 API 变更
全局状态 你无意中改过的配置
工作目录 相对路径指向不同地方

关键洞察:代码的"行为"不是代码自带的属性。它属于"代码 + 环境"这个整体。

18.2 认知科学里的对应物

编码特异性原则(encoding specificity principle)(Tulving & Thomson, 1973):

提取的效果取决于"提取时的线索"和"编码时的情境"匹配程度。

现象 说明
情境依存记忆(context-dependent) 在同一个房间学的,在同一个房间考得更好
状态依存记忆(state-dependent) 在某种情绪/药物状态下学的,同样状态下更容易回忆
移植效应(transfer-appropriate processing) 学习时的加工方式要和考试要求匹配
气味/声音线索 特定气味能显著提高回忆率

和"环境管理"的对照:

编程 认知
代码 + 环境 → 行为 记忆 + 情境 → 提取
换环境 → 行为可能变 换情境 → 回忆变差
requirements.txt 冻结环境 回到编码时的情境能提高提取
隔离环境消除干扰 减少情境变化减少遗忘

⭐ 这个对照有实际的用处:
"复习要在和考试相似的情境下",这不是迷信,
是编码特异性原则的直接推论。
如果你考试在教室、复习在宿舍,你可能损失一部分成绩,纯粹因为情境不匹配。
(但要注意:情境依存效应通常不大,不要指望靠这个逆天改命。)

18.3 一个更深的对照:认知的"语境依赖性"

认知科学里"语境"无处不在:

现象 语境的作用
词汇识别 "bank"在"river"和"money"语境下加工不同
范畴判断 企鹅是"鸟"吗?取决于语境
句子理解 同一个词在不同句子里激活不同的意义
推理 沃森选择任务里,语境能大幅改变正确率
情绪知觉 同一个表情在不同情境下判断不同

"同一刺激,不同语境,不同反应",
这和"同一代码,不同环境,不同行为"是同一个结构。

⚠️ 诚实说明:
- 编码特异性原则和情境依存记忆是认知心理学的重要发现,
有大量实验支持(Godden & Baddeley 1975 的水下实验是经典),
但效应的强度在不同条件下差异很大,有时很小。
- "情境依存"和"编码特异性"是相关但不同的概念,
本节的概括是简化版。
- 程序环境与认知语境的对照是教学类比,
不是说"大脑是虚拟机"或"记忆是数据库"。请以文献为准。


19 编程作业 S16

第 11 题参考写法:

#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""环境体检:把"我这台机器上是什么情况"一次性打印出来。

遇到"在我电脑上能跑"的问题时,先让对方跑这个脚本。
"""

import platform
import sys
from pathlib import Path

# 关心的包(按需增删)
PACKAGES = ["numpy", "pandas", "matplotlib", "scipy", "sympy"]


def main():
    print("=" * 46)
    print("环境体检报告")
    print("=" * 46)

    print(f"Python 版本   : {sys.version.split()[0]}")
    print(f"解释器路径    : {sys.executable}")
    print(f"操作系统      : {platform.system()} {platform.release()}")
    print(f"当前工作目录  : {Path.cwd()}")
    print(f"脚本所在目录  : {Path(__file__).resolve().parent}")

    print()
    print("第三方包:")
    import importlib.metadata as md
    for name in PACKAGES:
        try:
            print(f"  {name:<12} {md.version(name)}")
        except md.PackageNotFoundError:
            print(f"  {name:<12} ⚠️ 未安装")

    print()
    print("提示:如果某个包装了但版本不对,")
    print("      对照 requirements.txt 重新 pip install -r requirements.txt")


if __name__ == "__main__":
    main()

输出大概是这样:

==============================================
环境体检报告
==============================================
Python 版本   : 3.14.0
解释器路径    : /Users/mamingyu/miniconda3/bin/python
操作系统      : Darwin 25.6.0
当前工作目录  : /Users/mamingyu/Desktop/大一
脚本所在目录  : /Users/mamingyu/Desktop/大一

第三方包:
  numpy        2.5.3
  pandas       3.0.5
  matplotlib   3.11.1
  scipy        1.18.1
  sympy        1.14.0

提示:如果某个包装了但版本不对,
      对照 requirements.txt 重新 pip install -r requirements.txt

💡 把 检查环境.py 放进项目里,它能一次性回答"是环境问题还是代码问题"。
遇到"跑不了"的问题,先让对方跑它,比来回问十句都快。

第 9 题答案:

回复三条:
1. 你 `python -V` 是什么版本?
   (Python 版本不匹配会导致各种诡异问题)

2. 你 `pip list | grep pandas` 有没有输出?
   没有 → pip install -r requirements.txt
   有但版本不对 → 同上,用清单里的版本

3. 你是从项目根目录运行的吗?
   (相对路径依赖工作目录;从别的目录跑会 FileNotFoundError)

如果这三条都没问题,让 TA 跑 检查环境.py,把输出发过来。

本阶段自评

这是最后一份自评,问题多一些,它同时是一次总复习。

  1. 数学:SVD 的完整形式是什么?$U$、$\Sigma$、$V^T$ 各自的形状和性质?
  2. 数学:SVD 的几何意义(三个动作)是什么?
  3. 数学:奇异值和 $A^TA$ 的特征值是什么关系?
  4. 数学:为什么"任何矩阵都有 SVD"比"可对角化"适用范围广?举一个没有实特征值但有奇异值的矩阵
  5. 数学:对称矩阵的奇异值和特征值差在哪?为什么?
  6. 数学:Eckart–Young 定理说了什么?为什么它保证低秩近似是最优的?
  7. 数学:伪逆解决什么问题?它和线性回归的关系是什么?
  8. 数学:把"行列式、秩、特征值、奇异值"四个概念串成一句话
  9. 编程:虚拟环境解决的核心问题是什么?激活的本质是什么?
  10. 编程:venv 和 conda 各适合什么场景?
  11. 编程:pip freeze 全量清单和手写清单各自的优缺点?
  12. 编程:.venv/ 为什么绝对不能提交?
  13. 技能:复现一个项目的七个步骤是什么?为什么第 6 步是"先跑测试"?
  14. 技能:判断"可复现"的标准是什么(一句话)?
  15. 技能:朋友说"跑不了",你问哪三个问题?
  16. 连接:编码特异性原则是什么?它对你的复习方式有什么实际启示?
  17. 总复习:16 张技能卡里,你觉得最有用的是哪三张?为什么?
  18. 总复习:16 个阶段里,哪个概念你到现在还有点虚?(诚实回答,然后回去补)

全部答得上 → 大一上主线完成。

但这不等于结束:
- 把 16 份作业补完(这是最重要的事)
- 把阶段 14 的项目推上 GitHub
- 把 2 份速查表用起来(编程代码 + 数学公式)
- 然后按《学习路线》进入大一下

进度可以慢,但不能跳过作业。

最后更新:2026-09-26