系列 · 线性代数 · 第 10 篇

线性代数(十):矩阵范数与条件数——数值计算的健康体检

条件数是线性系统的'健康体检报告':它告诉你输入端的微小扰动会不会被放大成输出端的灾难。本章从向量范数、矩阵范数讲到谱范数、条件数与谱半径,剖析数值不稳定的根源,并给出预条件这一治本之策。

困扰工程师的那个问题#

方程没错,算法也没错,为什么算出来的结果完全不对?

线性代数(十):矩阵范数与条件数——数值计算的健康体检 — 章节概览图

罪魁祸首通常是一个叫 条件数 的数字。它衡量的是线性系统有多“敏感”——输入的一点微小扰动,会不会被放大成输出中的灾难性误差。要讨论条件数,我们首先需要一种方法来度量向量和矩阵的“大小”,这正是范数的作用。

本章内容#

  • 向量范数($L^1$$L^2$$L^\infty$ )及其单位球的几何形状。
  • 矩阵范数:Frobenius 范数、诱导范数,以及至关重要的谱范数。
  • 条件数 $\kappa(A) = \sigma_{\max}/\sigma_{\min}$ 的真正含义。
  • 为何病态矩阵(例如 Hilbert 矩阵)会让双精度算术彻底失效。
  • 条件数如何界定输入误差的放大程度。
  • 谱半径与迭代法的收敛性。
  • 预条件处理:如何驯服病态系统。

前置知识#


向量范数:衡量“大小”#

什么是范数?#

向量空间上的 范数 是一个函数 $\|\cdot\|$ ,满足以下三条公理:

  1. 非负性$\|\vec{x}\| \geq 0$ ,且等号成立当且仅当 $\vec{x} = \vec{0}$
  2. 绝对齐次性$\|c\vec{x}\| = |c|\,\|\vec{x}\|$
  3. 三角不等式$\|\vec{x} + \vec{y}\| \leq \|\vec{x}\| + \|\vec{y}\|$

这些性质契合我们对“大小”的日常直觉:没有负的尺寸;放大两倍,尺寸也翻倍;绕路永远不会比直线更短。

必须掌握的三种范数#

对于 $\vec{x} \in \mathbb{R}^n$

$$ \|\vec{x}\|_1 = |x_1| + |x_2| + \cdots + |x_n|. $$

想象一辆出租车只能在网格状街道上行驶;从原点到 $(3,4)$ 需要走 $3+4=7$ 个街区。

$$ \|\vec{x}\|_2 = \sqrt{x_1^2 + x_2^2 + \cdots + x_n^2}. $$

这是“乌鸦飞直线”的距离:乌鸦从原点飞到 $(3,4)$ 只需 $\sqrt{9+16}=5$

$$ \|\vec{x}\|_\infty = \max_i |x_i|. $$

国际象棋中的国王每次可朝八个方向移动一格;从 $(0,0)$$(3,4)$ 最少只需 $\max(3,4)=4$ 步,因为它能同时沿对角线和正交方向前进。

单位球的几何形状#

每种范数都定义了自己的单位球 $\{\vec{x} : \|\vec{x}\| \leq 1\}$

  • $L^1$ :二维是菱形,三维是八面体——尖角正好落在坐标轴上。
  • $L^2$ :二维是圆,三维是球——完美旋转对称。
  • $L^\infty$ :二维是正方形,三维是立方体——平面与坐标轴对齐。

$p$ 从 1 增大到 $\infty$ ,单位球会从菱形逐渐变为圆形,再扩展成正方形。$L^1$ 单位球的尖角正是 LASSO 回归能产生稀疏解的原因:损失函数的等高线往往最先接触 $L^1$ 约束的尖角,而这些尖角位于坐标轴上,意味着某些坐标会被强制设为零。

向量范数:单位球的形状随 p 变化

范数等价性#

$$ c\|\vec{x}\|_a \leq \|\vec{x}\|_b \leq C\|\vec{x}\|_a. $$

不同范数就像不同的尺子,测量的是同一个向量。若一个序列在某范数下收敛,则在所有范数下都收敛。选择哪种范数纯粹出于便利性或物理意义——但一旦涉及收敛速度,范数的选择又变得重要起来。

矩阵范数:这个变换有多“猛”?#

Frobenius 范数 —— 把矩阵当成一个长向量#

$$ \|A\|_F = \sqrt{\sum_{i,j} a_{ij}^2}. $$

只需将所有元素展平成一个长向量,再计算其 $L^2$ 范数即可。

图像类比:把 $A$ 看作一张灰度图像,每个元素代表一个像素强度。Frobenius 范数就是这张图像的 总能量。无论是图像压缩误差,还是神经网络中的权重更新幅度,它都是衡量“两个矩阵有多不同”的标准尺度。

$$ \|A\|_F = \sqrt{\sigma_1^2 + \sigma_2^2 + \cdots + \sigma_r^2}. $$

因此,Frobenius 范数是对 所有 奇异值的平方和开根号——它关心的是矩阵在每个方向上的拉伸。

诱导范数 —— 最大放大倍数#

$$ \|A\| = \max_{\|\vec{x}\| = 1} \|A\vec{x}\|. $$

可以把 $A$ 想象成一个放大镜:诱导范数问的是,“这个镜头最多能把东西放大多少倍?”如果 $\|A\|_2 = 3$ ,说明存在某个输入方向,经 $A$ 变换后长度变为原来的三倍。

最常见的三种诱导范数如下:

范数公式计算方式
$\mid A\mid_1$ (列和)$\max_j \sum_i \mid a_{ij}\mid$对每列取绝对值之和,再取最大值
$\mid A\mid_2$ (谱范数)$\sigma_{\max}(A)$最大奇异值
$\mid A\mid_\infty$ (行和)$\max_i \sum_j \mid a_{ij}\mid$对每行取绝对值之和,再取最大值

记忆口诀:1 → 列和,$\infty$ → 行和。

谱范数 —— 明星选手#

谱范数 $\|A\|_2 = \sigma_1$ 是最实用的矩阵范数:

  • 它等于 最大奇异值
  • 几何上,$A$ 将单位球映射为椭球;$\|A\|_2$ 就是该椭球最长半轴的长度。
  • 它刻画了 $A$最大放大因子

算子范数:椭圆最长半轴

上面两张图讲清了整个故事。左边是单位圆及其两个极值方向 $v_{\max}$$v_{\min}$ ;右边是 $A$ 将圆旋转拉伸后的椭圆——橙色箭头落在最长半轴上,长度为 $\sigma_{\max} = \|A\|_2$ ,而绿色箭头被压缩至长度 $\sigma_{\min}$

Frobenius vs 谱范数:两种“大小”观#

这两种范数回答的是关于同一矩阵的不同问题。谱范数关注 峰值拉伸——适用于你担心最坏情况的场景;Frobenius 范数关注 整体拉伸——适用于你关心平均行为的情形。

Frobenius 范数 vs 谱范数

图中所示矩阵,$\|A\|_2 = \sigma_1 \approx 2.78$ ,而 $\|A\|_F \approx 2.93$ 。两者接近是因为 $\sigma_2$ 相对较小;若矩阵有多个相近的奇异值,二者差距会显著拉大。

次可乘性#

$$ \|AB\| \leq \|A\|\,\|B\|. $$

连续应用两次变换,其总放大倍数不会超过各自放大倍数的乘积。这一不等式是证明迭代算法收敛性的关键,也控制着深度网络中重复矩阵乘法的最坏误差。

条件数:一份健康报告#

定义#

$$ \kappa(A) = \|A\|\,\|A^{-1}\|. $$ $$ \kappa_2(A) = \frac{\sigma_{\max}}{\sigma_{\min}}. $$

即最大与最小奇异值的比值。

过敏类比

  • $\kappa \approx 1$ :免疫系统健康,吹点风毫无影响。
  • $\kappa \approx 10^{10}$ :严重过敏,一粒花粉就可能引发休克。

条件数:良态 vs 病态

良态矩阵将输入圆映射为近似圆形的椭圆(虚线灰圆与绿色椭圆几乎重合);而病态矩阵则将其压成一根针:在输入空间中,某些方向上矩阵几乎无响应,因此求逆时必须将这些微弱输出大幅放大——而噪声也会随之被放大。

关键性质#

  • 下界$\kappa(A) \geq 1$ ,仅当 $A$ 是正交矩阵的标量倍时取等。
  • 正交不变性:若 $Q$ 正交,则 $\kappa(QA) = \kappa(A)$ ——旋转和反射不改变条件数。
  • 尺度不变性$\kappa(\alpha A) = \kappa(A)$ ——决定健康的是椭圆的 形状,而非大小。
  • 求逆对称性$\kappa(A^{-1}) = \kappa(A)$ ——求解与求逆难度相同。

一句话总结几何意义#

$$ \kappa = \frac{\text{最长半轴}}{\text{最短半轴}}. $$

$\kappa = 1$ 表示椭圆是圆;$\kappa = \infty$ 表示椭圆退化为线段(矩阵奇异)。

简单例子$A = I$$\kappa = 1$ ;而 $B = \begin{pmatrix} 1 & 0 \\ 0 & 10^{-5} \end{pmatrix}$$\kappa = 10^5$ ——它在一个方向上压缩了 $10^5$ 倍。

病态矩阵:数值计算的噩梦#

线性代数(十):矩阵范数与条件数——数值计算的健康体检 — 章节小结图

Hilbert 矩阵#

$$ H_{ij} = \frac{1}{i+j-1}. $$

其条件数增长速度极其恐怖。

阶数 $n$$\kappa(H_n)$双精度下可靠有效数字
5$\sim 10^{5}$约 10 位
10$\sim 10^{13}$约 3 位
12$\sim 10^{16}$基本为 0
15$\sim 10^{18}$完全无用
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
import numpy as np
from scipy.linalg import hilbert

n = 12
H = hilbert(n)
x_true = np.ones(n)
b = H @ x_true
x_hat = np.linalg.solve(H, b)

print(f"kappa(H_{n})    = {np.linalg.cond(H):.2e}")
print(f"相对误差       = {np.linalg.norm(x_true - x_hat) / np.linalg.norm(x_true):.2e}")

$n=12$ 时,相对误差已达量级 1:即使算法正确,结果也可能 100% 错误。

可视化“爆炸”#

奇异值谱与 Hilbert 矩阵的 κ(n) 增长

左图展示了两个 $20\times20$ 矩阵:一个良性矩阵的奇异值谱平坦,而病态矩阵的奇异值跨越六个数量级。右图绘制了 $\kappa_2(H_n)$ 的对数曲线——几乎是条直线,每增加两阶,条件数约翻一番。红色虚线是双精度的极限 $10^{16}$ ;超过此线,任何 IEEE 754 双精度算法都无能为力。

病态从何而来?#

  • 多项式拟合:高次拟合产生的 Vandermonde 矩阵,其条件数随次数指数增长。
  • 细密网格:有限元网格细化 $h$ 倍,条件数约增大 $h^{-2}$ 倍。
  • 正规方程$\kappa(A^TA) = \kappa(A)^2$ ——将最小二乘问题转为正规方程会使条件数平方,这常被称为“数值线性代数的原罪”。
  • 近奇异:两行或两列“几乎”线性相关。

误差分析:条件数如何放大误差#

右端项扰动#

假设求解 $A\vec{x} = \vec{b}$ ,但右端项被小扰动 $\delta\vec{b}$ 污染。解的变化有多大?

$$ \frac{\|\delta\vec{x}\|}{\|\vec{x}\|} \;\leq\; \kappa(A)\,\frac{\|\delta\vec{b}\|}{\|\vec{b}\|}. $$

条件数就是相对误差的 最大放大因子

具体而言:

  • $\kappa = 10$ :1% 输入误差最多导致 10% 输出误差。
  • $\kappa = 10^{10}$ :1% 输入误差可能导致 $10^8$ % 输出误差——结果纯属噪声。
  • $\kappa = 10^{16}$ :即使机器精度舍入误差(约 $10^{-16}$ )也足以摧毁答案。

实验可验证此现象。下图对 $b$ 施加相同幅度(2%)但方向各异的扰动,绘制出解 $x + \delta x$ 的分布云。

扰动敏感性:微小 δb,巨大 δx

$\kappa \approx 2$ 时,解云是一个围绕真解 $x$ 的小环——放大因子约 1;而当 $\kappa \approx 200$ 时,同样的相对扰动使解沿一条细长对角线滑动——最坏情况下的放大因子大致就是 $\kappa$ 本身。

矩阵扰动#

$$ \frac{\|\delta\vec{x}\|}{\|\vec{x}\|} \;\leq\; \kappa(A)\left(\frac{\|\delta A\|}{\|A\|} + \frac{\|\delta\vec{b}\|}{\|\vec{b}\|}\right). $$

仍是同一个条件数,同样惩罚:矩阵元素的误差也会被 $\kappa(A)$ 放大。

经验法则:损失的有效数字#

$\kappa(A) \approx 10^k$ ,在双精度下求解 $A\vec{x} = \vec{b}$ 会损失约 $k$ 位有效数字。

$\kappa(A)$损失位数可靠位数
$10^{4}$4≈ 12
$10^{8}$8≈ 8
$10^{12}$12≈ 4
$10^{16}$160(无用)

谱半径与迭代收敛#

定义#

$$ \rho(A) = \max_i |\lambda_i|. $$

它被任意矩阵范数所控制:$\rho(A) \leq \|A\|$

收敛准则#

不动点迭代 $\vec{x}_{k+1} = B\vec{x}_k + \vec{c}$ 从任意初值收敛 当且仅当 $\rho(B) < 1$

淋浴类比:调节水温时,若系统稳定($\rho < 1$ ),每次调整都更接近理想温度;若不稳定($\rho \geq 1$ ),水温会在冰凉与滚烫间振荡。

收敛速度:每次迭代误差约缩小为 $\rho(B)$ 倍,达到误差 $\epsilon$ 需约 $\log\epsilon / \log\rho(B)$ 次迭代。$\rho$ 越小,收敛越快。

Neumann 级数#

$$ (I - A)^{-1} = I + A + A^2 + A^3 + \cdots, $$

这是 $\frac{1}{1-x} = 1 + x + x^2 + \cdots$ 的矩阵版本,在摄动分析和小矩阵逆近似中很有用。

数值稳定性:选择合适的算法#

为何正规方程危险#

$$ \kappa(A^TA) = \kappa(A)^2. $$

$A$$\kappa = 10^6$ ,正规方程的 $\kappa$ 就变成 $10^{12}$ ——这无异于自造灾难。

QR:稳定替代方案#

$A = QR$ 进行 QR 分解,再解 $R\hat{\vec{x}} = Q^T\vec{b}$ 。由于 $Q$ 正交,系统条件数保持为 $\kappa(A)$ ,不会平方。现代几乎所有最小二乘例程都默认采用此法。

SVD:最稳但最贵#

通过伪逆 $\hat{\vec{x}} = V\Sigma^+U^T\vec{b}$ 求解。SVD 能优雅处理秩亏问题,返回最小范数解,并免费提供奇异值。代价是计算量约为 QR 的 2–3 倍。

方法对比#

下图对同一 $50\times50$ 系统 $Ax = b$ ,在条件数从 $10^2$ 增至 $10^{14}$ 时,比较三种方法的误差:

数值稳定性:三种解法的误差 vs 条件数

三点值得注意:(1) QR 与 SVD 曲线基本重合,始终沿 $\varepsilon_{\text{mach}}\cdot\kappa$ 线上升;(2) 正规方程曲线在双对数图上斜率是它们的两倍——这正是 $\kappa^2$ 的代价;(3) 当 $\kappa \approx 10^8$ 时,正规方程已越过“无用”线,而 QR/SVD 仍保留约八位可靠数字。务必在数据到来前选好算法。

预条件处理:驯服病态系统#

核心思想#

$$ M^{-1}A\vec{x} = M^{-1}\vec{b}. $$

$M \approx A$ ,则 $M^{-1}A \approx I$ ,新系统的 $\kappa(M^{-1}A) \approx 1$

类比:用厨房秤称大象显然不行——但若已知大象重量大致范围,只需用秤测偏差即可。预条件处理正是将问题从不合适的尺度转移到工具可处理的范围内。

常见预条件子#

预条件子$M$优点缺点
Jacobi$\text{diag}(A)$实现简单,完全并行改善有限
Gauss–Seidel$A$ 的下三角部分优于 Jacobi难以并行
不完全 LU (ILU)稀疏近似 $LU$强大通用需控制填充
不完全 Cholesky (ICC)稀疏近似 $LL^T$存储减半仅限对称正定

权衡普遍存在:更强的预条件子减少迭代次数,但单次迭代成本更高。最优预条件子常利用问题结构(如 PDE 用几何多重网格,并行求解用区域分解等)。

应用实例#

有限元分析#

结构力学中的刚度矩阵 $K$ 满足 $\kappa(K) \propto h^{-2}$ ,其中 $h$ 为网格尺寸。细化网格虽能捕捉更多细节,却使矩阵更难求解。若网格高度不均或材料属性差异巨大(如钢筋混凝土中钢与混凝土刚度相差数个量级),问题会更严重——此时预条件处理必不可少。

图像去模糊#

$$ \min_{\vec{x}} \|B\vec{x} - \vec{y}\|^2 + \lambda\|\vec{x}\|^2. $$

正则化参数 $\lambda$ 在保真度与稳定性间权衡,实际上将条件数中的 $\sigma_{\min}$ 替换为 $\sqrt{\sigma_{\min}^2 + \lambda}$ ——即使很小的 $\lambda$ 也能挽救问题。

深度学习:梯度消失与爆炸#

权重矩阵的谱范数控制信号传播:

  • 多层中 $\|W\|_2 > 1$ → 前向信号与反向梯度指数爆炸。
  • 多层中 $\|W\|_2 < 1$ → 信号指数衰减,梯度消失。

解决方案如批归一化、精心初始化(Xavier、He)、残差连接、梯度裁剪,本质上都是让网络 Jacobian 的有效谱范数接近 1。

机器学习中的正则化#

$$ \kappa(X^TX + \lambda I) = \frac{\sigma_1^2 + \lambda}{\sigma_n^2 + \lambda}. $$

即使 $\lambda$ 不大,也能使条件数下降数个量级。类似思想(Levenberg–Marquardt、信赖域、权重衰减)在优化中反复出现。

Python 示例#

计算范数和条件数#

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
import numpy as np

A = np.array([[1, 2],
              [3, 4]], dtype=float)

print(f"Frobenius 范数 : {np.linalg.norm(A, 'fro'):.4f}")
print(f"谱范数         : {np.linalg.norm(A, 2):.4f}")
print(f"1-范数         : {np.linalg.norm(A, 1):.4f}")
print(f"无穷范数       : {np.linalg.norm(A, np.inf):.4f}")
print(f"条件数         : {np.linalg.cond(A):.4f}")

Hilbert 矩阵实验#

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
from scipy.linalg import hilbert
import matplotlib.pyplot as plt

sizes = range(2, 16)
conds = [np.linalg.cond(hilbert(n)) for n in sizes]

plt.semilogy(list(sizes), conds, "o-")
plt.xlabel("矩阵阶数 n")
plt.ylabel("条件数")
plt.axhline(1e16, ls="--")  # 双精度极限
plt.title("Hilbert 矩阵:条件数随阶数 n 急剧增大")
plt.show()

对比最小二乘解法#

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
def compare_methods(cond_target=1e8, n=50, seed=0):
    rng = np.random.default_rng(seed)
    Q1, _ = np.linalg.qr(rng.standard_normal((n, n)))
    Q2, _ = np.linalg.qr(rng.standard_normal((n, n)))
    s = np.logspace(0, -np.log10(cond_target), n)
    A = Q1 @ np.diag(s) @ Q2

    x_true = rng.standard_normal(n)
    b = A @ x_true

    x_normal = np.linalg.solve(A.T @ A, A.T @ b)        # 风险较大的方法
    Q, R = np.linalg.qr(A); x_qr = np.linalg.solve(R, Q.T @ b)
    x_svd = np.linalg.lstsq(A, b, rcond=None)[0]

    print(f"kappa(A)        : {np.linalg.cond(A):.2e}")
    print(f"正规方程误差    : {np.linalg.norm(x_normal - x_true):.2e}")
    print(f"QR 方法误差     : {np.linalg.norm(x_qr - x_true):.2e}")
    print(f"SVD 方法误差    : {np.linalg.norm(x_svd - x_true):.2e}")

compare_methods()

练习题#

基础题#

  1. $\vec{x} = (3, -4, 0, 1)$ ,计算 $\|\vec{x}\|_1$$\|\vec{x}\|_2$$\|\vec{x}\|_\infty$
  2. $A = \begin{pmatrix} 1 & 2 \\ 3 & 4 \end{pmatrix}$ ,计算 Frobenius 范数与谱范数。
  3. 对对角矩阵 $D = \text{diag}(d_1, \ldots, d_n)$ ,证明 $\kappa_2(D) = \max|d_i| / \min|d_i|$

进阶题#

  1. 证明:对任意矩阵范数,$\rho(A) \leq \|A\|$
  2. 证明:若 $\|A\| < 1$ (诱导范数),则 $I - A$ 可逆。
  3. 证明:正交矩阵 $Q$ 满足 $\kappa_2(Q) = 1$
  4. np.linalg.cond 估计 $n = 2, \ldots, 15$$\kappa(H_n)$ ,在对数坐标下拟合直线,求经验增长率。

编程题#

  1. 不用 np.linalg.norm,从零实现 $\|A\|_1$$\|A\|_2$$\|A\|_\infty$
  2. 复现本章稳定性图:对最小二乘问题,绘制正规方程、QR、SVD 的误差随条件数变化曲线。
  3. 实现带与不带对角预条件的 Jacobi 迭代,绘制迭代次数与迭代矩阵谱半径的关系。

实用指南#

$\kappa(A)$风险建议
$< 10^{4}$标准方法即可
$10^{4}$$10^{8}$验证结果;优先选 QR 或 SVD,避免正规方程
$10^{8}$$10^{12}$加正则化或使用预条件子
$> 10^{12}$极高退一步,重新建模问题

总结#

概念关键公式直觉
向量范数$\mid\vec{x}\mid_p = (\sum \mid x_i\mid^p)^{1/p}$“这个向量有多大?”
Frobenius 范数$\mid A\mid_F = \sqrt{\sum a_{ij}^2}$矩阵的总能量
谱范数$\mid A\mid_2 = \sigma_{\max}$最大放大能力
条件数$\kappa = \sigma_{\max}/\sigma_{\min}$误差放大的上限
谱半径$\rho(A) = \max\mid\lambda_i\mid$控制迭代收敛性
正规方程$\kappa(A^TA) = \kappa(A)^2$为何它们危险

参考文献#

  • Golub, G. H. & Van Loan, C. F. (2013). Matrix Computations, 4th ed.
  • Trefethen, L. N. & Bau, D. (1997). Numerical Linear Algebra.
  • Higham, N. J. (2002). Accuracy and Stability of Numerical Algorithms, 2nd ed.
  • Demmel, J. W. (1997). Applied Numerical Linear Algebra.
  • Saad, Y. (2003). Iterative Methods for Sparse Linear Systems, 2nd ed.

本文是“线性代数的本质”系列第 10 章,共 18 章。

本系列

线性代数 18 篇

  1. 01 线性代数(一):向量的本质——不仅仅是箭头
  2. 02 线性代数(二):线性组合与向量空间
  3. 03 线性代数(三):矩阵作为线性变换
  4. 04 线性代数(四):行列式的秘密
  5. 05 线性代数(五):线性方程组与列空间
  6. 06 线性代数(六):特征值与特征向量
  7. 07 线性代数(七):正交性与投影——当向量互不干扰
  8. 08 线性代数(八):对称矩阵与二次型
  9. 09 线性代数(九):奇异值分解 SVD
  10. 10 线性代数(十):矩阵范数与条件数——数值计算的健康体检 当前
  11. 11 线性代数(十一):矩阵微积分与优化——从梯度到反向传播
  12. 12 线性代数(十二):稀疏矩阵与压缩感知——少即是多的数学奇迹
  13. 13 线性代数(十三):张量与多线性代数——从标量到高维数据立方体
  14. 14 线性代数(十四):随机矩阵理论——混沌中的秩序
  15. 15 线性代数(十五):机器学习中的线性代数——从 PCA 到推荐系统
  16. 16 线性代数(十六):深度学习中的线性代数——从全连接到 Transformer
  17. 17 线性代数(十七):计算机视觉中的线性代数——从像素到三维重建
  18. 18 线性代数(十八):前沿应用与总结——量子计算、GNN、大模型,与十八章回望

读有所得?

GitHub 关注我 → 新文周更

GitHub