系列:《从小白到 AI 大模型开发工程师的进阶之路》 技术点:AI-0215 奇异值分解 主人公:小蓝伞 前置:AI-0213 矩阵运算;建议先读第60期特征值与特征向量 本期产出:一个能报告各截断秩误差、验证满秩重构并拒绝一维输入的实验脚本
小蓝伞准备把一张矩阵“压瘦”,却遇到两个疑问:矩形矩阵没有普通的方阵特征分解形式,怎样拆;只留下最大的几个分量,损失究竟是多少?这是教学情境,不是生产事故。本文用一份手工构造的四行三列矩阵跑通 SVD、低秩重构和输入失败验证。实测保留两个分量时相对残差约为百分之六点八二,保留全部三个分量时达到浮点精度内的重构。读者得到的是明确的误差口径和可复制程序,而不是“压缩一定提升预测效果”的承诺。
一、小蓝伞遇到的问题
输入矩阵有四行三列,行可理解为四个观测,列可理解为三个数值字段。但本例只用来验证线性代数,不给列附会业务意义。矩阵第三行全为零,其他行中既有对角结构也有交叉项,因此并非简单地删除某一列就能得到最佳低秩表达。
小蓝伞最初想直接取最大的特征值。排查前先检查形状:特征方程要求同一空间中的方阵作用,本例的输入、输出空间维数不同。换用 SVD 后,程序能分解矩形矩阵,接下来需要验证三个问题:输出形状是否匹配;截断的分量是否按降序选择;误差是绝对残差还是相对残差。缺任何一项,都可能出现“代码能跑,说明说错了”的情况。
二、先给结论
实数矩阵可以写成三部分相乘:左奇异向量、奇异值对角矩阵、右奇异向量的转置。用 NumPy 的紧凑输出完成低秩重构时,正确式子是先截取对应列和行,再乘回原形状。
本例采用
full_matrices=False,避免生成重构不需要的额外正交方向。奇异值按降序排列,截断保留前几个分量;奇异值数量上限是行列数中的较小值。
用 Frobenius 范数衡量全部元素的联合残差,同时报告相对残差。
零矩阵可以重构,但相对误差的分母为零,不能直接相除后把无效结果当成准确率。
一维输入在本篇封装中明确拒绝。NumPy 支持更高维批量 SVD,本例没有扩展到该接口。
重构误差下降只证明当前矩阵保存得更完整,不证明推荐、分类或预测一定更准确。
三、本文要解决什么
| 项目 | 本期约定 |
|---|---|
| 输入 | 手工构造的实数二维矩阵,不含缺失值 |
| 输出 | 奇异值、各秩绝对残差、各秩相对残差 |
| 环境 | Windows 11、Python 3.13.9、NumPy 2.3.5 |
| 成功判据 | 满秩重构接近原矩阵;截断误差随秩增加而不增加 |
| 失败判据 | 一维数组不能进入二维重构接口;秩不得越界 |
| 不在范围 | 大规模稀疏求解、性能排名、真实推荐评测 |
四、前置准备
使用独立练习目录,下面路径仅为建议,可以替换成自己的目录。程序不覆盖已有数据,不需要管理员权限。依赖安装需要正常访问包源;本地已有对应版本时直接使用即可。
New-Item -ItemType Directory -Force D:\ai-learning\issue-61 Set-Location D:\ai-learning\issue-61 python -m venv .venv .\.venv\Scripts\python.exe -m pip install numpy==2.3.5 .\.venv\Scripts\python.exe --version .\.venv\Scripts\python.exe -c "import numpy; print(numpy.__version__)"
这些新建环境步骤是读者复现步骤,本次运行使用现有 Python 环境,未宣称重新安装过依赖。不必激活环境,显式调用其中的解释器可以减少终端混用风险。若安装受网络或版本兼容条件限制,先记录错误并核对官方支持范围,不随意更换来源或扩大安装权限。
五、核心原理
从矩阵变换理解分解
对一个有四行三列的实数矩阵,紧凑 SVD 给出:左因子四行三列,奇异值向量长度三,右转置因子三行三列。奇异值向量不能直接当成二维矩阵使用;可以构造对角矩阵,也可以通过广播将左因子的每一列乘上对应奇异值。然后乘右转置因子,结果仍是四行三列。
本地 NumPy 2.3.5 官方函数说明给出的重构约定是u @ np.diag(s) @ vh,等价于(u * s) @ vh。本篇变量名用vt表示实数情形的转置。若将来处理复数,接口返回的是共轭转置,应把这个区别保留下来,不能机械使用普通转置。
截断不是删掉某几个原始字段
截断保留的是分量方向,不是原始列名单。每个分量都由一个左方向、一个伸缩量和一个右方向共同组成。取前两个分量时,截取左因子的前两列、奇异值前两项和右因子的前两行,三者必须同时变化。只截奇异值而不改其他因子,会造成形状错误。
错误认知是“第一奇异值最大,所以第一个业务字段最重要”。正确认知是“大奇异值对应一个更强的矩阵变换方向”。这个方向可能由多个字段组合而成,而且会受到原始尺度影响。若金额用元、时间用秒,不能跳过单位说明就给字段排业务价值。
怎样给误差一个可核对的定义
绝对残差是所有元素差值平方求和,再开平方,也就是 Frobenius 范数。相对残差是绝对残差除以原矩阵的同一范数。因此“保留两个分量后误差百分之六点八二”指整个矩阵的相对范数残差,不能解释为“每个元素都错百分之六点八二”,更不能解释为分类错误率。
也可以报告保留能量比例,即保留奇异值平方和除以全部平方和。它和相对残差有关,但不是同一个数字:保留能量高,并不意味着相对残差按相同比例降低。选择指标时应先决定要限制残差、保留能量还是下游任务损失,再计算对应口径。
六、完整实践
目录只需要一个main.py。数据由代码直接构造,无需下载图片或读取上一期文件。将以下完整代码保存为 UTF-8:
import numpy as np def reconstruct(matrix: np.ndarray, rank: int) -> np.ndarray: """按指定截断秩重构矩阵,并显式检查输入边界。""" if matrix.ndim != 2: raise ValueError("matrix 必须是二维数组") u, singular_values, vt = np.linalg.svd(matrix, full_matrices=False) if not 0 <= rank <= singular_values.size: raise ValueError("rank 必须位于 0 到 min(m, n) 之间") if rank == 0: return np.zeros_like(matrix) return (u[:, :rank] * singular_values[:rank]) @ vt[:rank, :] matrix = np.array( [[3.0, 0.0, 1.0], [0.0, 2.0, 0.0], [0.0, 0.0, 0.0], [1.0, 1.0, 0.0]], dtype=np.float64, ) u, singular_values, vt = np.linalg.svd(matrix, full_matrices=False) print("shape", matrix.shape, "singular", np.round(singular_values, 6).tolist()) for rank in range(singular_values.size + 1): restored = reconstruct(matrix, rank) residual = np.linalg.norm(matrix - restored, ord="fro") relative = residual / np.linalg.norm(matrix, ord="fro") print("rank", rank, "residual", round(float(residual), 8), "relative", round(float(relative), 8)) try: reconstruct(np.array([1.0, 2.0]), 1) except ValueError as exc: print("invalid", type(exc).__name__, str(exc))在练习目录运行:
.\.venv\Scripts\python.exe main.py
成功时会输出矩阵形状、三个奇异值、四种截断秩和最后的一维输入错误。本次实际数字见下一节。这里显式捕获异常仅用于展示失败样本,不是生产中吞掉异常的建议;业务调用应根据契约选择向上传递或转换为可定位错误。
七、可复现失败案例
故意把二维矩阵替换为一维数组,直接调用 NumPy SVD 时得到“数组至少必须二维”的线性代数异常。影响是调用方无法区分“维度输入错误”和“算法未收敛”。最初误判可能是某个数值不适合求解,但检查形状后就能定位原因:维度已经在进入算法前失效。
修复是在封装入口判断维度,并抛出明确的ValueError。原一维失败样本重新运行后输出“matrix 必须是二维数组”,正常矩阵仍能生成全部误差行,构成一条可复验的修复链。不要为了消除报错自动把任意一维输入重塑成矩阵;一行样本和一列样本含义不同,应由调用者决定。
另一个建议验证的边界是非法秩。把秩改为负数或超过三,应得到越界异常,而不是静默得到少量分量。当前函数约定调用者传入整数秩,若要作为公共接口,还需要拒绝布尔值、小数、空维度和非有限元素,并给这些输入补运行记录;本期没有冒称这些扩展全部执行完成。
八、实验设计与数据
本次是确定性功能实验,不是计时基准。固定四行三列输入、双精度类型和依赖版本,仅改变截断秩。正常样本、零秩边界、满秩边界和一维失败样本均在本机执行。奇异值约为三点三二五六一一、二点二零五八九八和零点二七二六三零。
| 截断秩 | 绝对残差 | 相对残差 |
|---|---|---|
| 0 | 4.00000000 | 100.0000% |
| 1 | 2.22268188 | 55.5670% |
| 2 | 0.27262966 | 6.8157% |
| 3 | 打印为0.00000000 | 打印为0.00000000 |
满秩行打印为零,是保留八位小数后的表现,不是计算机采用无限精度。实际验收应使用容差比较,不应要求数组逐元素位级一致。原矩阵范数恰为四,因此每行相对残差可以直接用绝对残差除以四复核。
若预算只允许保留一个分量,本例会留下超过一半的相对残差;允许两个分量时降至约百分之六点八二。这个表能帮助选择重构秩,却没有留出集标签,所以不能宣称第三个分量使预测更准。未来接推荐数据时,缺失评分也不能随意当作真实零值,否则拟合对象已经换了。
九、常见问题与避坑
紧凑 SVD 是否只计算前几个奇异值?不是。紧凑输出省去多余正交方向,但本例仍得到全部三个奇异值。若只需要大矩阵的少量分量,需评估专门的截断或随机算法,并另行验证近似误差。
低秩一定节省存储吗?不一定。本例原矩阵十二个数;按秩二保存两个方向因子和奇异值,需要十六个数。低秩表征的存储收益应比较因子总元素数,而不是只看秩小于列数。
零矩阵的相对误差怎样写?先报告绝对误差为零,再声明相对口径不适用,或明确约定该特殊输入的返回值。不能把除零产生的无效值送进报告。
十、系统与库的差异
本次只在 Windows、Python 3.13.9 和 NumPy 2.3.5 执行。不同 BLAS 后端、硬件或版本可能改变浮点末位和运行时间;相同奇异值对应的向量还可能同时翻转符号。比较时关注重构、范数和方向子空间,不以向量每个元素的正负号判定算法错误。Linux 或 macOS 的环境目录和解释器路径需自行调整,本文没有给出跨系统性能结论。
十一、验证清单
运行版本命令,确认实际解释器和 NumPy 版本。
原矩阵应为四行三列,紧凑左因子四行三列,右因子三行三列。
各秩残差不应因增加分量而上升;若上升,先检查三因子的切片是否一致。
满秩用绝对容差验证重构,而不是依赖格式化后的零。
一维样本应明确失败;失败时先检查输入形状。
记录绝对误差、相对误差和尺度,不把三者混成一个准确率。
十二、面试题与追问
SVD 为什么适合矩形矩阵?答:左右方向分别对应两个维数不同的空间,中间用非负奇异值连接。追问:四行三列矩阵紧凑输出各是什么形状?
截断重构应该切哪几部分?答:左因子前几列、奇异值前几项、右因子前几行一起切。追问:只截奇异值会怎样?
相对残差等于每个元素的误差率吗?答:不等于,它是整体范数的比值。追问:零矩阵为何需特殊处理?
紧凑输出等于部分求解吗?答:不等于,仍计算较小维度对应的全部奇异值。追问:什么时候考虑随机或截断求解器?
误差越低是否越有业务价值?答:不能直接推出,需独立业务目标和留出评测。追问:缺失评分当零会改变什么?
十三、小蓝伞的工程金句
“低秩保留的是组合方向,不是原始字段排行榜。”这句话对应本期三因子切片规则。另一个可操作判断是:“先问压缩了多少存储,再说压缩是否划算。”它对应小矩阵因子存储反而增长的例子。
十四、本篇技术清单与下一期
本期完成矩形矩阵分解、逐秩重构、范数误差和维度失败排查。下一期 AI-0216 导数与偏导数,将从“数据如何分解”转向“函数在当前位置怎样变化”,以解析求导和有限差分建立梯度的检验方法。沿系列继续读,可以把分解、求导和后续优化连接成同一条计算链。你的矩阵压缩预算更关注存储、重构误差,还是下游任务损失?先明确这一项再选秩。
官方资料
NumPy SVD:重构式、输出形状和奇异值顺序;本次核对本地安装包对应的官方函数说明。
NumPy 范数:Frobenius 范数参数;本次核对本地安装包函数说明和实际输出。
scikit-learn 分解与 PCA:中心化、缩放区别及随机分解的适用方向;2026-10-13 查阅。该资料不作为本例 NumPy 版本的接口证明。
适用边界
本文只验证手工小矩阵、实数双精度、二维接口及逐秩功能行为。代码按整数秩、有限非空矩阵使用;公共服务还需补参数类型、非有限值和资源上限。图像压缩、真实推荐预测、随机算法和大矩阵性能均未在本期实测。