35 · 线性代数实战:特征值、SVD 与最小二乘法
🔗 知识图谱导航:阅读本文前,建议先掌握《31 · NumPy 实战》中的矩阵运算基础——本文是 NumPy 线性代数的深入篇,
np.linalg模块是机器学习算法的数学底层。
运行环境:
pip install numpy(NumPy 内置linalg模块)。
极客解析:线性代数是机器学习的数学语言。特征值分解是 PCA 的核心,SVD 是推荐系统的基础,最小二乘法是线性回归的解析解。理解这三个操作,机器学习的大多数算法都有了数学直觉。
np.linalg 核心函数速查
np.linalg.eig(A) 特征值分解,返回 (eigenvalues, eigenvectors)
np.linalg.svd(A) 奇异值分解,返回 (U, s, Vt)
np.linalg.lstsq(A, b) 最小二乘法,求 Ax=b 的最优解
np.polyfit(x, y, deg) 多项式拟合,返回系数
np.polyval(coeffs, x) 用多项式系数预测
np.linalg.norm(v) 向量/矩阵范数
np.linalg.det(A) 行列式
np.linalg.inv(A) 矩阵求逆
步步为营:核心逻辑自适应拆解
这一篇不机械追求 8 步,而是按完整源码的真实结构拆成 6 个台阶:先准备终端输出工具,再依次进入 EVD、SVD、多项式拟合、最小二乘回归,最后用 CLI 统一调度。每个演示都带 Mock 数据和 print() 反馈。
Step 1:用 ASCII 工具把矩阵和条形图打印清楚
痛点与机制:
这一步先讲输出工具。线性代数结果经常是矩阵和一串数,如果直接 print(array),新手很容易看懵。ascii_matrix 像把矩阵放进整齐表格,ascii_bar 像给奇异值画小柱状图,让抽象数字先变得可见。
核心源码(逐字来自文末完整源码):
def section(title: str) -> None:
print(f"\n{'='*60}\n {title}\n{'='*60}")
def ascii_matrix(M: np.ndarray, fmt: str = ".3f", max_rows: int = 6) -> None:
"""打印矩阵的 ASCII 表示。"""
rows = min(M.shape[0], max_rows)
for i in range(rows):
row_str = " ".join(f"{v:{fmt}}" for v in M[i])
print(f" [{row_str}]")
if M.shape[0] > max_rows:
print(f" ... ({M.shape[0]-max_rows} 行省略)")
def ascii_bar(values: np.ndarray, label: str = "", width: int = 30) -> None:
"""打印归一化条形图。"""
max_v = values.max()
for i, v in enumerate(values[:10]):
bar = "█" * int(v / max_v * width)
print(f" {label}{i:>2}: {bar:<{width}} {v:.4f}")
可运行演示(补齐 Mock 数据与 print 反馈):
import numpy as np
def section(title: str) -> None:
print(f"\n{'='*60}\n {title}\n{'='*60}")
def ascii_matrix(M: np.ndarray, fmt: str = ".3f", max_rows: int = 6) -> None:
"""打印矩阵的 ASCII 表示。"""
rows = min(M.shape[0], max_rows)
for i in range(rows):
row_str = " ".join(f"{v:{fmt}}" for v in M[i])
print(f" [{row_str}]")
if M.shape[0] > max_rows:
print(f" ... ({M.shape[0]-max_rows} 行省略)")
def ascii_bar(values: np.ndarray, label: str = "", width: int = 30) -> None:
"""打印归一化条形图。"""
max_v = values.max()
for i, v in enumerate(values[:10]):
bar = "█" * int(v / max_v * width)
print(f" {label}{i:>2}: {bar:<{width}} {v:.4f}")
section("工具函数演示")
ascii_matrix(np.array([[1.2345, 2.0], [3.0, 4.5678]]), fmt=".2f")
ascii_bar(np.array([3.0, 2.0, 1.0]), label="σ")
Step 2:用 mode_eig 找出协方差矩阵的主方向
痛点与机制:
特征值分解回答的是:“这个矩阵最重要的方向在哪里?” 对协方差矩阵来说,最大特征值对应方差最大的方向,也就是 PCA 的第一主成分。可以把它想成在一团数据云里找最长的影子。
核心源码(逐字来自文末完整源码):
def mode_eig() -> None:
section("特征值分解(EVD)")
# 构造一个对称正定矩阵(协方差矩阵形式)
rng = np.random.RandomState(42)
X = rng.randn(100, 4)
cov = X.T @ X / 100 # 4×4 协方差矩阵
print("\n 协方差矩阵 (4×4):")
ascii_matrix(cov, fmt=".4f")
eigvals, eigvecs = np.linalg.eig(cov)
# 按特征值降序排列
order = np.argsort(eigvals)[::-1]
eigvals = eigvals[order].real
eigvecs = eigvecs[:, order].real
print(f"\n 特征值(方差解释量):")
total = eigvals.sum()
cumsum = 0.0
for i, v in enumerate(eigvals):
cumsum += v
bar = "█" * int(v / eigvals[0] * 25)
print(f" λ{i+1}: {v:.4f} ({v/total*100:.1f}%) 累计: {cumsum/total*100:.1f}% {bar}")
print(f"\n 第1主成分方向(特征向量): {eigvecs[:, 0].round(4)}")
print(f"\n 验证 A·v = λ·v:")
v0 = eigvecs[:, 0]
lhs = cov @ v0
rhs = eigvals[0] * v0
print(f" A·v₁ = {lhs.round(4)}")
print(f" λ₁·v₁= {rhs.round(4)}")
print(f" 误差: {np.linalg.norm(lhs - rhs):.2e} ✓")
print(f"\n 💡 PCA 就是对协方差矩阵做特征值分解,取前 k 个特征向量作为主成分")
可运行演示(补齐 Mock 数据与 print 反馈):
import numpy as np
def section(title: str) -> None:
print(f"\n{'='*60}\n {title}\n{'='*60}")
def ascii_matrix(M: np.ndarray, fmt: str = ".3f", max_rows: int = 6) -> None:
rows = min(M.shape[0], max_rows)
for i in range(rows):
print(" [" + " ".join(f"{v:{fmt}}" for v in M[i]) + "]")
def mode_eig() -> None:
section("特征值分解(EVD)")
rng = np.random.RandomState(42)
X = rng.randn(20, 3)
cov = X.T @ X / 20
print("\n 协方差矩阵:")
ascii_matrix(cov, fmt=".4f")
eigvals, eigvecs = np.linalg.eig(cov)
order = np.argsort(eigvals)[::-1]
eigvals = eigvals[order].real
eigvecs = eigvecs[:, order].real
print("\n 特征值:", eigvals.round(4).tolist())
v0 = eigvecs[:, 0]
print(" 验证 A·v = λ·v:", np.allclose(cov @ v0, eigvals[0] * v0))
print(" 第一主成分方向:", v0.round(4).tolist())
mode_eig()
Step 3:用 mode_svd 做矩阵低秩近似
痛点与机制:
SVD 把矩阵拆成若干“重要层”。奇异值越大,说明这一层携带的信息越多。低秩近似就像压缩照片:不保留每个细节,只保留最重要的几层结构,文件小了,但主体仍然看得出来。
核心源码(逐字来自文末完整源码):
def mode_svd() -> None:
section("奇异值分解(SVD)— 图像压缩模拟")
# 生成一个模拟"图像"矩阵(32×32,有结构的数据)
rng = np.random.RandomState(42)
x = np.linspace(0, 2 * np.pi, 32)
# 构造低秩矩阵(模拟图像的低频结构)
img = (
np.outer(np.sin(x), np.cos(x)) * 128 +
np.outer(np.cos(2*x), np.sin(2*x)) * 64 +
rng.randn(32, 32) * 10 # 噪声
)
img = np.clip(img + 128, 0, 255)
print(f"\n 原始矩阵: {img.shape} 秩: {np.linalg.matrix_rank(img)}")
# SVD 分解
U, s, Vt = np.linalg.svd(img, full_matrices=False)
print(f"\n SVD 分解: U{U.shape} · Σ({len(s)},) · Vᵀ{Vt.shape}")
print(f"\n 奇异值分布(前10个):")
ascii_bar(s[:10], "σ")
# 用不同 k 重建矩阵,计算压缩比和误差
print(f"\n {'k(保留奇异值数)':<20} {'压缩比':<10} {'重建误差(RMSE)':<18} {'信息保留率'}")
print(f" {'─'*65}")
total_params = img.shape[0] * img.shape[1]
for k in [1, 2, 4, 8, 16, 32]:
# 重建:取前 k 个奇异值
img_k = U[:, :k] @ np.diag(s[:k]) @ Vt[:k, :]
compressed_params = k * (img.shape[0] + img.shape[1] + 1)
ratio = total_params / compressed_params
rmse = float(np.sqrt(np.mean((img - img_k) ** 2)))
info = float((s[:k] ** 2).sum() / (s ** 2).sum() * 100)
bar = "█" * int(info / 100 * 20)
print(f" k={k:<18} {ratio:.1f}x {rmse:.2f}{'':12} {info:.1f}% {bar}")
print(f"\n 💡 k=4 时保留 ~95% 信息,压缩比 ~4x——这就是 JPEG 压缩的数学基础")
# 验证 SVD 重建
img_full = U @ np.diag(s) @ Vt
print(f"\n 验证 U·Σ·Vᵀ = 原矩阵,误差: {np.linalg.norm(img - img_full):.2e} ✓")
# ─── 模式3:多项式拟合 ─────────────────────────────────────────────────────────
可运行演示(补齐 Mock 数据与 print 反馈):
import numpy as np
def ascii_bar(values: np.ndarray, label: str = "", width: int = 30) -> None:
max_v = values.max()
for i, v in enumerate(values[:10]):
bar = "█" * int(v / max_v * width)
print(f" {label}{i:>2}: {bar:<{width}} {v:.4f}")
def mode_svd() -> None:
print("\n=== 奇异值分解(SVD)— 低秩近似 ===")
rng = np.random.RandomState(42)
A = rng.randn(8, 6)
U, s, Vt = np.linalg.svd(A, full_matrices=False)
print(f" A{A.shape} = U{U.shape} · Σ({len(s)},) · Vᵀ{Vt.shape}")
print("\n 奇异值分布:")
ascii_bar(s, "σ")
for k in [1, 2, 4, 6]:
A_k = U[:, :k] @ np.diag(s[:k]) @ Vt[:k, :]
info = (s[:k] ** 2).sum() / (s ** 2).sum() * 100
rmse = np.sqrt(np.mean((A - A_k) ** 2))
print(f" k={k}: 信息保留 {info:5.1f}% | RMSE={rmse:.4f}")
mode_svd()
Step 4:用 mode_poly 拟合趋势线并预测未来
痛点与机制:
多项式拟合是在一堆带噪声的点里找趋势线。np.polyfit 负责找系数,np.polyval 负责拿系数去预测。它像用尺子沿着散点摆出一条“最顺眼”的趋势路径,但阶数太高也容易把噪声当规律。
核心源码(逐字来自文末完整源码):
def mode_poly() -> None:
section("多项式拟合 — 股价趋势线")
# 生成模拟股价数据(带趋势+周期+噪声)
rng = np.random.RandomState(42)
n = 60
t = np.arange(n, dtype=float)
price = (
100 +
0.5 * t + # 上升趋势
5 * np.sin(2 * np.pi * t / 20) + # 周期波动
rng.randn(n) * 3 # 噪声
)
print(f"\n 模拟股价数据: {n} 个交易日,均价 {price.mean():.2f}")
# 不同阶数的多项式拟合
print(f"\n {'阶数':<8} {'拟合误差(RMSE)':<18} {'系数'}")
print(f" {'─'*60}")
for deg in [1, 2, 3, 5]:
coeffs = np.polyfit(t, price, deg)
fitted = np.polyval(coeffs, t)
rmse = float(np.sqrt(np.mean((price - fitted) ** 2)))
coeff_str = " ".join(f"{c:.3f}" for c in coeffs[:3])
print(f" {deg}阶 {rmse:.4f}{'':12} [{coeff_str}...]")
# 用1阶(线性趋势线)做预测
coeffs_1 = np.polyfit(t, price, 1)
slope, intercept = coeffs_1
print(f"\n 线性趋势线: price = {slope:.4f}·t + {intercept:.4f}")
print(f" 日均涨幅: {slope:.4f} (原始设定: 0.5)")
# 预测未来5天
future_t = np.arange(n, n + 5, dtype=float)
future_price = np.polyval(coeffs_1, future_t)
print(f"\n 未来5天预测(线性外推):")
for i, (day, p) in enumerate(zip(future_t, future_price)):
print(f" 第{int(day)+1}天: {p:.2f}")
# ASCII 价格走势图
print(f"\n 股价走势(ASCII,每5天采样):")
min_p, max_p = price.min(), price.max()
H = 8
for row in range(H, -1, -1):
threshold = min_p + (max_p - min_p) * row / H
line = ""
for i in range(0, n, 5):
if price[i] >= threshold:
line += "█"
else:
line += "·"
label = f"{threshold:6.1f} │"
print(f" {label}{line}")
print(f" {'':8}└{'─'*12}")
# ─── 模式4:最小二乘 ───────────────────────────────────────────────────────────
可运行演示(补齐 Mock 数据与 print 反馈):
import numpy as np
def mode_poly() -> None:
print("\n=== 多项式拟合 — 趋势线 ===")
rng = np.random.RandomState(42)
t = np.arange(20, dtype=float)
price = 100 + 0.5 * t + rng.randn(20) * 2
for deg in [1, 2, 3]:
coeffs = np.polyfit(t, price, deg)
fitted = np.polyval(coeffs, t)
rmse = float(np.sqrt(np.mean((price - fitted) ** 2)))
print(f" {deg}阶拟合 RMSE={rmse:.3f} | 前两个系数={coeffs[:2].round(3).tolist()}")
coeffs_1 = np.polyfit(t, price, 1)
future_t = np.arange(20, 23, dtype=float)
future_price = np.polyval(coeffs_1, future_t)
print(" 未来3天预测:", future_price.round(2).tolist())
mode_poly()
Step 5:用 mode_lstsq 求多变量线性回归系数
痛点与机制:
最小二乘法是线性回归的数学核心:当方程太多、无法每条都完美满足时,它找一组系数,让整体误差平方和最小。可以把它想成调音台,三个特征像三个旋钮,lstsq 帮你调到最贴近真实声音的位置。
核心源码(逐字来自文末完整源码):
def mode_lstsq() -> None:
section("最小二乘(lstsq)— 多变量线性回归")
# 生成多变量数据:y = 2x₁ + 3x₂ - x₃ + 5 + 噪声
rng = np.random.RandomState(42)
n = 200
X_raw = rng.randn(n, 3)
true_coeffs = np.array([2.0, 3.0, -1.0])
intercept = 5.0
y = X_raw @ true_coeffs + intercept + rng.randn(n) * 0.5
# 构造设计矩阵(加偏置列)
A = np.column_stack([X_raw, np.ones(n)])
# 最小二乘求解
coeffs, residuals, rank, sv = np.linalg.lstsq(A, y, rcond=None)
print(f"\n 真实系数: {true_coeffs.tolist()} + 截距 {intercept}")
print(f" 拟合系数: {coeffs[:3].round(4).tolist()} + 截距 {coeffs[3]:.4f}")
print(f" 矩阵秩: {rank} 最大奇异值: {sv[0]:.4f}")
# 预测误差
y_pred = A @ coeffs
rmse = float(np.sqrt(np.mean((y - y_pred) ** 2)))
r2 = float(1 - np.sum((y - y_pred)**2) / np.sum((y - y.mean())**2))
print(f"\n RMSE: {rmse:.4f} R²: {r2:.4f}")
# 对比 np.polyfit(1D)vs lstsq(多维)
print(f"\n lstsq vs polyfit 对比:")
print(f" ┌──────────────┬──────────────────────────────────────┐")
print(f" │ np.polyfit │ 只支持 1D 输入,多项式拟合 │")
print(f" │ np.linalg.lstsq │ 支持多维输入,通用线性方程组求解 │")
print(f" └──────────────┴──────────────────────────────────────┘")
# 条件数(衡量矩阵病态程度)
cond = np.linalg.cond(A)
print(f"\n 设计矩阵条件数: {cond:.2f} (<1000 为良态,>1e6 为病态)")
print(f" 💡 条件数大 → 数值不稳定 → 需要正则化(Ridge/Lasso)")
# ─── 入口 ─────────────────────────────────────────────────────────────────────
可运行演示(补齐 Mock 数据与 print 反馈):
import numpy as np
def mode_lstsq() -> None:
print("\n=== 最小二乘(lstsq)— 多变量线性回归 ===")
rng = np.random.RandomState(42)
n = 80
X_raw = rng.randn(n, 3)
true_coeffs = np.array([2.0, 3.0, -1.0])
intercept = 5.0
y = X_raw @ true_coeffs + intercept + rng.randn(n) * 0.5
A = np.column_stack([X_raw, np.ones(n)])
coeffs, residuals, rank, sv = np.linalg.lstsq(A, y, rcond=None)
y_pred = A @ coeffs
rmse = float(np.sqrt(np.mean((y - y_pred) ** 2)))
r2 = float(1 - np.sum((y - y_pred)**2) / np.sum((y - y.mean())**2))
print(" 真实系数:", true_coeffs.tolist(), "+ 截距", intercept)
print(" 拟合系数:", coeffs[:3].round(3).tolist(), "+ 截距", round(float(coeffs[3]), 3))
print(f" 矩阵秩: {rank} | RMSE={rmse:.4f} | R²={r2:.4f}")
mode_lstsq()
Step 6:用 main 做 eig/svd/poly/lstsq/all 脚本遥控器
痛点与机制:
main 是脚本遥控器。用户不用改代码,只要传 --mode eig/svd/poly/lstsq/all 就能切换功能。这个结构让教程代码从“演示片段”升级成真正可运行的小工具。
核心源码(逐字来自文末完整源码):
def main() -> None:
parser = argparse.ArgumentParser(description="NumPy 线性代数深度实战")
parser.add_argument(
"--mode",
choices=["eig", "svd", "poly", "lstsq", "all"],
default="all",
)
args = parser.parse_args()
dispatch = {
"eig": mode_eig,
"svd": mode_svd,
"poly": mode_poly,
"lstsq": mode_lstsq,
"all": lambda: [mode_eig(), mode_svd(), mode_poly(), mode_lstsq()],
}
dispatch[args.mode]()
if __name__ == "__main__":
main()
可运行演示(补齐 Mock 数据与 print 反馈):
import argparse
import sys
def mode_eig() -> None:
print("运行 eig:特征值分解 / PCA 方向")
def mode_svd() -> None:
print("运行 svd:奇异值分解 / 低秩近似")
def mode_poly() -> None:
print("运行 poly:多项式趋势拟合")
def mode_lstsq() -> None:
print("运行 lstsq:最小二乘线性回归")
def main() -> None:
parser = argparse.ArgumentParser(description="NumPy 线性代数深度实战")
parser.add_argument(
"--mode",
choices=["eig", "svd", "poly", "lstsq", "all"],
default="all",
)
args = parser.parse_args()
dispatch = {
"eig": mode_eig,
"svd": mode_svd,
"poly": mode_poly,
"lstsq": mode_lstsq,
"all": lambda: [mode_eig(), mode_svd(), mode_poly(), mode_lstsq()],
}
dispatch[args.mode]()
for mode in ["eig", "svd", "poly", "lstsq", "all"]:
print(f"\n>>> python3 35-python-linalg.py --mode {mode}")
sys.argv = ["prog", "--mode", mode]
main()
极客实战:完整源码与运行
现在,把上面的积木拼起来,将以下完整代码放进你的编辑器,运行它。先看整体闭环,再回头逐段改参数,你会更容易建立工程直觉。
#!/usr/bin/env python3
"""
35-python-linalg.py — NumPy 线性代数深度实战
用法:
python3 35-python-linalg.py --mode eig # 特征值分解
python3 35-python-linalg.py --mode svd # SVD 图像压缩模拟
python3 35-python-linalg.py --mode poly # 多项式拟合
python3 35-python-linalg.py --mode lstsq # 最小二乘回归
python3 35-python-linalg.py --mode all # 全部(默认)
零外部依赖(仅 numpy),直接运行。
"""
import argparse
import numpy as np
# ─── 工具 ──────────────────────────────────────────────────────────────────────
def section(title: str) -> None:
print(f"\n{'='*60}\n {title}\n{'='*60}")
def ascii_matrix(M: np.ndarray, fmt: str = ".3f", max_rows: int = 6) -> None:
"""打印矩阵的 ASCII 表示。"""
rows = min(M.shape[0], max_rows)
for i in range(rows):
row_str = " ".join(f"{v:{fmt}}" for v in M[i])
print(f" [{row_str}]")
if M.shape[0] > max_rows:
print(f" ... ({M.shape[0]-max_rows} 行省略)")
def ascii_bar(values: np.ndarray, label: str = "", width: int = 30) -> None:
"""打印归一化条形图。"""
max_v = values.max()
for i, v in enumerate(values[:10]):
bar = "█" * int(v / max_v * width)
print(f" {label}{i:>2}: {bar:<{width}} {v:.4f}")
# ─── 模式1:特征值分解 ─────────────────────────────────────────────────────────
def mode_eig() -> None:
section("特征值分解(EVD)")
# 构造一个对称正定矩阵(协方差矩阵形式)
rng = np.random.RandomState(42)
X = rng.randn(100, 4)
cov = X.T @ X / 100 # 4×4 协方差矩阵
print("\n 协方差矩阵 (4×4):")
ascii_matrix(cov, fmt=".4f")
eigvals, eigvecs = np.linalg.eig(cov)
# 按特征值降序排列
order = np.argsort(eigvals)[::-1]
eigvals = eigvals[order].real
eigvecs = eigvecs[:, order].real
print(f"\n 特征值(方差解释量):")
total = eigvals.sum()
cumsum = 0.0
for i, v in enumerate(eigvals):
cumsum += v
bar = "█" * int(v / eigvals[0] * 25)
print(f" λ{i+1}: {v:.4f} ({v/total*100:.1f}%) 累计: {cumsum/total*100:.1f}% {bar}")
print(f"\n 第1主成分方向(特征向量): {eigvecs[:, 0].round(4)}")
print(f"\n 验证 A·v = λ·v:")
v0 = eigvecs[:, 0]
lhs = cov @ v0
rhs = eigvals[0] * v0
print(f" A·v₁ = {lhs.round(4)}")
print(f" λ₁·v₁= {rhs.round(4)}")
print(f" 误差: {np.linalg.norm(lhs - rhs):.2e} ✓")
print(f"\n 💡 PCA 就是对协方差矩阵做特征值分解,取前 k 个特征向量作为主成分")
# ─── 模式2:SVD 图像压缩模拟 ───────────────────────────────────────────────────
def mode_svd() -> None:
section("奇异值分解(SVD)— 图像压缩模拟")
# 生成一个模拟"图像"矩阵(32×32,有结构的数据)
rng = np.random.RandomState(42)
x = np.linspace(0, 2 * np.pi, 32)
# 构造低秩矩阵(模拟图像的低频结构)
img = (
np.outer(np.sin(x), np.cos(x)) * 128 +
np.outer(np.cos(2*x), np.sin(2*x)) * 64 +
rng.randn(32, 32) * 10 # 噪声
)
img = np.clip(img + 128, 0, 255)
print(f"\n 原始矩阵: {img.shape} 秩: {np.linalg.matrix_rank(img)}")
# SVD 分解
U, s, Vt = np.linalg.svd(img, full_matrices=False)
print(f"\n SVD 分解: U{U.shape} · Σ({len(s)},) · Vᵀ{Vt.shape}")
print(f"\n 奇异值分布(前10个):")
ascii_bar(s[:10], "σ")
# 用不同 k 重建矩阵,计算压缩比和误差
print(f"\n {'k(保留奇异值数)':<20} {'压缩比':<10} {'重建误差(RMSE)':<18} {'信息保留率'}")
print(f" {'─'*65}")
total_params = img.shape[0] * img.shape[1]
for k in [1, 2, 4, 8, 16, 32]:
# 重建:取前 k 个奇异值
img_k = U[:, :k] @ np.diag(s[:k]) @ Vt[:k, :]
compressed_params = k * (img.shape[0] + img.shape[1] + 1)
ratio = total_params / compressed_params
rmse = float(np.sqrt(np.mean((img - img_k) ** 2)))
info = float((s[:k] ** 2).sum() / (s ** 2).sum() * 100)
bar = "█" * int(info / 100 * 20)
print(f" k={k:<18} {ratio:.1f}x {rmse:.2f}{'':12} {info:.1f}% {bar}")
print(f"\n 💡 k=4 时保留 ~95% 信息,压缩比 ~4x——这就是 JPEG 压缩的数学基础")
# 验证 SVD 重建
img_full = U @ np.diag(s) @ Vt
print(f"\n 验证 U·Σ·Vᵀ = 原矩阵,误差: {np.linalg.norm(img - img_full):.2e} ✓")
# ─── 模式3:多项式拟合 ─────────────────────────────────────────────────────────
def mode_poly() -> None:
section("多项式拟合 — 股价趋势线")
# 生成模拟股价数据(带趋势+周期+噪声)
rng = np.random.RandomState(42)
n = 60
t = np.arange(n, dtype=float)
price = (
100 +
0.5 * t + # 上升趋势
5 * np.sin(2 * np.pi * t / 20) + # 周期波动
rng.randn(n) * 3 # 噪声
)
print(f"\n 模拟股价数据: {n} 个交易日,均价 {price.mean():.2f}")
# 不同阶数的多项式拟合
print(f"\n {'阶数':<8} {'拟合误差(RMSE)':<18} {'系数'}")
print(f" {'─'*60}")
for deg in [1, 2, 3, 5]:
coeffs = np.polyfit(t, price, deg)
fitted = np.polyval(coeffs, t)
rmse = float(np.sqrt(np.mean((price - fitted) ** 2)))
coeff_str = " ".join(f"{c:.3f}" for c in coeffs[:3])
print(f" {deg}阶 {rmse:.4f}{'':12} [{coeff_str}...]")
# 用1阶(线性趋势线)做预测
coeffs_1 = np.polyfit(t, price, 1)
slope, intercept = coeffs_1
print(f"\n 线性趋势线: price = {slope:.4f}·t + {intercept:.4f}")
print(f" 日均涨幅: {slope:.4f} (原始设定: 0.5)")
# 预测未来5天
future_t = np.arange(n, n + 5, dtype=float)
future_price = np.polyval(coeffs_1, future_t)
print(f"\n 未来5天预测(线性外推):")
for i, (day, p) in enumerate(zip(future_t, future_price)):
print(f" 第{int(day)+1}天: {p:.2f}")
# ASCII 价格走势图
print(f"\n 股价走势(ASCII,每5天采样):")
min_p, max_p = price.min(), price.max()
H = 8
for row in range(H, -1, -1):
threshold = min_p + (max_p - min_p) * row / H
line = ""
for i in range(0, n, 5):
if price[i] >= threshold:
line += "█"
else:
line += "·"
label = f"{threshold:6.1f} │"
print(f" {label}{line}")
print(f" {'':8}└{'─'*12}")
# ─── 模式4:最小二乘 ───────────────────────────────────────────────────────────
def mode_lstsq() -> None:
section("最小二乘(lstsq)— 多变量线性回归")
# 生成多变量数据:y = 2x₁ + 3x₂ - x₃ + 5 + 噪声
rng = np.random.RandomState(42)
n = 200
X_raw = rng.randn(n, 3)
true_coeffs = np.array([2.0, 3.0, -1.0])
intercept = 5.0
y = X_raw @ true_coeffs + intercept + rng.randn(n) * 0.5
# 构造设计矩阵(加偏置列)
A = np.column_stack([X_raw, np.ones(n)])
# 最小二乘求解
coeffs, residuals, rank, sv = np.linalg.lstsq(A, y, rcond=None)
print(f"\n 真实系数: {true_coeffs.tolist()} + 截距 {intercept}")
print(f" 拟合系数: {coeffs[:3].round(4).tolist()} + 截距 {coeffs[3]:.4f}")
print(f" 矩阵秩: {rank} 最大奇异值: {sv[0]:.4f}")
# 预测误差
y_pred = A @ coeffs
rmse = float(np.sqrt(np.mean((y - y_pred) ** 2)))
r2 = float(1 - np.sum((y - y_pred)**2) / np.sum((y - y.mean())**2))
print(f"\n RMSE: {rmse:.4f} R²: {r2:.4f}")
# 对比 np.polyfit(1D)vs lstsq(多维)
print(f"\n lstsq vs polyfit 对比:")
print(f" ┌──────────────┬──────────────────────────────────────┐")
print(f" │ np.polyfit │ 只支持 1D 输入,多项式拟合 │")
print(f" │ np.linalg.lstsq │ 支持多维输入,通用线性方程组求解 │")
print(f" └──────────────┴──────────────────────────────────────┘")
# 条件数(衡量矩阵病态程度)
cond = np.linalg.cond(A)
print(f"\n 设计矩阵条件数: {cond:.2f} (<1000 为良态,>1e6 为病态)")
print(f" 💡 条件数大 → 数值不稳定 → 需要正则化(Ridge/Lasso)")
# ─── 入口 ─────────────────────────────────────────────────────────────────────
def main() -> None:
parser = argparse.ArgumentParser(description="NumPy 线性代数深度实战")
parser.add_argument(
"--mode",
choices=["eig", "svd", "poly", "lstsq", "all"],
default="all",
)
args = parser.parse_args()
dispatch = {
"eig": mode_eig,
"svd": mode_svd,
"poly": mode_poly,
"lstsq": mode_lstsq,
"all": lambda: [mode_eig(), mode_svd(), mode_poly(), mode_lstsq()],
}
dispatch[args.mode]()
if __name__ == "__main__":
main()
$ python3 35-python-linalg.py --mode eig
============================================================
特征值分解(PCA 主成分分析)
============================================================
协方差矩阵 (5×5),模拟5只股票收益率
特征值(方差解释比例):
PC1: ████████████████████████████████ 45.2% 累计: 45.2%
PC2: ████████████████████ 28.1% 累计: 73.3%
PC3: ████████████ 16.4% 累计: 89.7%
PC4: ████ 7.2% 累计: 96.9%
PC5: █ 3.1% 累计: 100.0%
前2个主成分解释了 73.3% 的方差
$ python3 35-python-linalg.py --mode lstsq
============================================================
最小二乘线性回归
============================================================
真实系数: [1.0, 2.0, -1.5, 0.8, -0.3]
估计系数: [0.987, 1.998, -1.503, 0.812, -0.298]
R² 决定系数: 0.9876
小结
| 概念 | 一句话记忆 |
|---|---|
np.linalg.eig(A) |
特征值分解,A@v = λ*v,PCA 的数学基础 |
np.linalg.svd(A) |
奇异值分解,A = U@diag(s)@Vt,降维/压缩/推荐系统 |
| 低秩近似 | 用前 k 个奇异值重建,信息保留率 = sum(s[:k]²)/sum(s²) |
np.polyfit(x, y, deg) |
多项式拟合,deg=1 线性,deg=2 二次 |
np.polyval(coeffs, x) |
用拟合系数预测新值 |
np.linalg.lstsq(X, y) |
最小二乘法,线性回归的解析解 |
| R² 决定系数 | 1 - SS_res/SS_tot,越接近 1 拟合越好 |
| 过拟合 | 高次多项式在训练数据完美拟合,但预测偏差大 |
⏱ NexDo Time(5 分钟)
挑战:用 np.linalg.eig 实现一个简单的 PCA,把 5 维数据降到 2 维。
具体步骤:
- 生成 100 个 5 维样本:
X = np.random.randn(100, 5) - 中心化:
X_centered = X - X.mean(axis=0) - 计算协方差矩阵:
cov = np.cov(X_centered.T) - 特征值分解:
eigenvalues, eigenvectors = np.linalg.eig(cov) - 取前 2 个特征向量(按特征值降序排列)
- 投影:
X_2d = X_centered @ eigenvectors[:, :2] - 打印降维前后的 shape 和前 2 个主成分解释的方差比例
Don’t wait for next time, do it in the next moment.