HOSVD与Tucker分解详解:用NumPy实现三阶张量分解与秩选择
简介面向张量分解学习者和数据分析研究者提供三阶张量HOSVD与Tucker分解的完整MATLAB实现及理论参考文献可帮助解决高维数据降维、特征提取和核心张量计算问题。压缩包共2个文件包含1个MATLAB源码文件与1个PDF文献资料分别对应分解算法的实际操作和理论推导整体大小约4.58MB轻量便于下载。目前已有1637人学习下载适合需要快速入门张量分解或复现相关实验的入门与进阶用户。读者可获得可直接运行的HOSVD/Tucker分解脚本通过实例理解奇异值分解在张量模式下的变体配套PDF则从理论上串联三阶张量表示、核心张量与因子矩阵的求解步骤为后续在图像多通道分析、推荐系统建模等场景落地提供基础。无论作为课堂作业参考还是科研代码起点这份资料都能节省自行推导和编码的时间。1. HOSVD 不是高维 SVD三阶张量分解到底拆出了什么很多人第一次听到“高阶奇异值分解HOSVD”第一反应是“矩阵 SVD 的高维扩展”。这个直觉一半对一半错。矩阵 SVD 把二维矩阵拆成左奇异向量、奇异值、右奇异向量而三阶张量有三个 mode行、列、深三个方向HOSVD 的输出是一个低秩核心张量加三个正交因子矩阵它们通过 mode-n 乘积耦合在一起。直接对张量每个切片做 SVD 再拼回去那叫 slice-wise SVD不是 HOSVD两者压缩效果和可解释性差很多。这份资源解决的是三件事看懂三阶张量的 mode-n 展开、用 NumPy 手写 HOSVD 和 Tucker 分解、在截断秩和重构误差之间找到可复现的调参路径。适合要做张量压缩、多通道信号降维、推荐系统特征提取的工程师和研究生也适合被张量分解文献符号劝退、想先跑通一个完整例子的新手。下面从矩阵 SVD 开始逐步把三阶张量分解的每一步讲透最后给出代码和五个翻车点。2. 从矩阵 SVD 到三阶张量mode-n 展开与奇异值谱2.1 矩阵 SVD 的直觉奇异值排序意味着什么先回到矩阵 SVD。对任意矩阵 A ∈ R^{I×J}奇异值分解写成 A U S V^T其中 U、V 都是正交矩阵S 是对角矩阵且奇异值从大到小排列。截断到前 r 个奇异值得到 A ≈ U[:, :r] S[:r, :r] Vt[:r, :]这个近似在 Frobenius 范数意义下是所有秩 r 近似里误差最小的这是 Eckart–Young 定理也是矩阵 SVD 最值钱的性质。奇异值谱直接告诉你数据在哪些方向上能量更集中。一个经验判断是看累积能量比sum(S[:k]^2) / sum(S^2) 0.9 时前 k 个方向通常已经保留了 90% 的能量。做矩阵降维时这个比例是最常用的截断依据。到了三阶张量事情不再这么简单。张量 X ∈ R^{I×J×K} 有三个方向每个方向都有自己的“奇异向量”但三个方向通过同一个核心张量耦合。你不能只算一次 SVD 就得到全部信息而是要把张量沿每个方向先展开成矩阵分别做 SVD再把结果组装起来。这就要引入 mode-n 展开。2.2 mode-n 展开把三阶张量拍平成矩阵mode-n 展开matricization是把张量的第 n 个维度保留为行其余维度按固定顺序合并为列。对三阶张量 X三种展开分别是mode-1 展开 X_(1) ∈ R^{I×(J·K)}固定第一维索引 i遍历 (j, k)mode-2 展开 X_(2) ∈ R^{J×(I·K)}固定 j遍历 (i, k)mode-3 展开 X_(3) ∈ R^{K×(I·J)}固定 k遍历 (i, j)关键坑在于列顺序。NumPy 的 reshape 按 C 顺序展开也就是最右边的轴变化最快。对三阶张量X.reshape(I, J*K) 恰好是 mode-1 展开因为内存布局天然就是 j 变化慢、k 变化快。但 mode-2 和 mode-3 必须先交换轴的顺序再 reshape否则列顺序会错乱。通用实现如下import numpy as np def mode_n_unfold(X, n): 三阶张量 X 沿 mode n 展开为矩阵。 n 0 对应 mode-1n 1 对应 mode-2n 2 对应 mode-3。 perm [n] [i for i in range(X.ndim) if i ! n] X_perm np.transpose(X, perm) # 把目标轴放到最前 rows X.shape[n] # 行数 目标轴长度 cols int(np.prod(X.shape) // rows) # 列数 其余维度乘积 return X_perm.reshape(rows, cols)这段代码的含义分三步perm 生成轴交换顺序transpose 把第 n 维挪到第一个位置reshape 把其余维度合并成列。以 X 形状 (4, 5, 6) 为例mode-2 展开后得到 (5, 4*6) (5, 24) 的矩阵。列的顺序是当行索引固定为 j 时列索引先遍历 i变化慢再遍历 k变化快这与标准张量展开定义一致。补充一点展开矩阵的列顺序有两种约定一种按上述 C 连续顺序另一种按完全相反的列优先顺序。两种约定得到的列重排不会改变奇异值和左奇异向量只影响右奇异向量的排列和后续列索引的解读。因此实际使用中只要保持一致选哪种约定都不影响 HOSVD 的因子矩阵和核心张量。这也解释了为什么不同文档里的展开定义看起来不一样但分解结果对得上。2.3 展开矩阵的 SVD为什么只取左奇异向量有了三个展开矩阵HOSVD 的核心动作就是对 X_(1)、X_(2)、X_(3) 分别做一次 SVD然后取左奇异向量作为该 mode 的因子矩阵。对 X_(1) ∈ R^{I×JK} 做 SVD得到 U1 ∈ R^{I×I}、S1、Vt1取前 R1 列得到 U1_r ∈ R^{I×R1}。同理得到 U2_r、U3_r。这三个矩阵就是 HOSVD 的因子矩阵。为什么取左奇异向量而不是右奇异向量关键在展开矩阵的行和列语义完全不同。行方向是当前 mode 自身的方向比如图像的宽度方向列方向是另外两个 mode 组合出来的联合方向。左奇异向量张成展开矩阵的行空间描述的是当前模式本身的基右奇异向量张成列空间描述的是另两个 mode 联合作用的方向。压缩或重构时需要每个 mode 自己的低维基所以取左奇异向量。另外有个细节值得点破三个展开矩阵分别做 SVD并不是把原张量当作一个大矩阵一次性分解。张量没有矩阵那样的单一奇异值谱它的信息分布在三个 mode 方向各自展开的结构里。这也是为什么 HOSVD 叫 High-Order SVD——它不是“高维矩阵的 SVD”而是“每个高阶方向各做一次 SVD 再耦合”。这里有个容易被忽略的细节每个展开矩阵的 SVD 都是独立做的单看每个 mode截断都是“该 mode 展开矩阵意义下的最优”。但三个截断组合起来并不等于张量全局最优近似先记住这个关键点后面第 5 章会展开这个坑。2.4 核心张量一次三路投影的结果因子矩阵确定了三套低维坐标轴下一步是把原始张量投影到这三个轴张成的子空间里得到核心张量 G。核心张量 G ∈ R^{R1×R2×R3}维度远小于原始张量。标准公式是 G X ×1 U1_r^T ×2 U2_r^T ×3 U3_r^T其中 ×n 表示 mode-n 乘积本质是张量沿第 n 维与矩阵做矩阵乘法。用 einsum 写最直白三个 mode 分别与 U1_r^T、U2_r^T、U3_r^T 做内积收缩掉原始维度保留 R1、R2、R3def project_core(X, U1, U2, U3): 三路投影出核心张量 G。 X: (I, J, K)U1: (I, R1)U2: (J, R2)U3: (K, R3) 返回 G: (R1, R2, R3) return np.einsum(ijk,ia,jb,kc-abc, X, U1, U2, U3)einsum 的下标 ijk,ia,jb,kc-abc 含义是X 的 i 轴与 U1 的 i 轴配对求和输出 a即 R1X 的 j 轴与 U2 的 j 轴配对输出 bX 的 k 轴与 U3 的 k 轴配对输出 c。一次调用完成三次 mode-n 投影。核心张量的物理意义是原始张量在新坐标系里的系数。它能压缩而不过度丢失跨 mode 的相关结构这正是 HOSVD 和逐切片 SVD 最本质的区别切片 SVD 忽略了 mode 间的耦合HOSVD 通过核心张量保留了耦合。3. HOSVD 与 Tucker 分解差在哪正交约束和“非最优截断”3.1 Tucker 分解的完整定义核张量与三个因子矩阵Tucker 分解是三阶张量分解里最通用的形式之一它把 X ∈ R^{I×J×K} 写成 X ≈ G ×1 A ×2 B ×3 C其中 A ∈ R^{I×R1}、B ∈ R^{J×R2}、C ∈ R^{K×R3}G ∈ R^{R1×R2×R3}。写成标量形式更直观X(i, j, k) ≈ Σ_a Σ_b Σ_c G(a, b, c) · A(i, a) · B(j, b) · C(k, c)这里的 A、B、C 叫作因子矩阵G 叫作核心张量。核心张量的大小 R1×R2×R3 决定了压缩程度理想情况下 R1 远小于 I、R2 远小于 J、R3 远小于 K存储量从 I·J·K 降到 R1·R2·R3 I·R1 J·R2 K·R3。Tucker 分解对因子矩阵没有正交要求只要列线性无关即可。这意味着同一组数据可以有很多套等价表示对核心张量做任意可逆变换 M同时对因子矩阵做对应的逆变换得到的结果不变。这个自由度既是 Tucker 的灵活性来源也是可解释性难做的原因之一。3.2 HOSVD 是 Tucker 的正交特例HOSVD 本质上是 Tucker 分解的一个特例它额外要求 A、B、C 列正交即 A^T A I、B^T B I、C^T C I。用第 2 章的方法得到的因子矩阵来自各自展开矩阵的 SVD 左奇异向量天然满足正交性。这个约束带来几个直接好处计算稳定SVD 得到的数值稳定性比任意迭代得到的非正交因子更好投影语义清晰因子矩阵是正交基核心张量是原张量在这组正交基上的坐标构造算法确定不用迭代一次 SVD 加一次投影就能完成分解代价是近似误差不受控。矩阵 SVD 截断有 Eckart–Young 定理保证全局最优但三阶张量没有类似的定理。截断 HOSVD 的误差是各 mode 截断误差的叠加上界而不是最小可能误差。表格对比对比项HOSVD最优低秩 Tucker因子矩阵列正交列线性独立即可构造算法逐 mode SVD一次完成需迭代优化如 HOOI重构误差有上界保证非全局最优可达局部最优误差单调下降计算成本低每次迭代要做多次 SVD典型用途初始化、压缩、特征提取追求最小重构误差时3.3 HOOI 迭代在 HOSVD 初始化上继续逼近如果重构误差是核心指标业界通行做法是用 HOOI高阶正交迭代在 HOSVD 结果上继续优化。HOOI 是交替最小二乘思路固定两个因子矩阵把张量投影到这两个方向的低维子空间然后对剩余方向做 SVD 更新第三个因子矩阵循环到误差收敛。核心迭代片段如下def hooi(X, ranks, n_iter20): # 用 HOSVD 作为初始化避免随机初始化带来的不确定性 _, factors hosvd(X, ranks) U1, U2, U3 factors for _ in range(n_iter): # 更新 U1: 固定 U2, U3把 X 沿 mode-2,3 投影 Y np.einsum(ijk,jb,kc-ibc, X, U2, U3) # (I, R2, R3) Y_mat Y.reshape(Y.shape[0], -1) # mode-1 展开 (I, R2*R3) U1 np.linalg.svd(Y_mat, full_matricesFalse)[0][:, :ranks[0]] # 更新 U2: 固定 U1, U3投影后取 mode-2 展开 Y np.einsum(ijk,ia,kc-ajc, X, U1, U3) # (R1, J, R3) Y_mat np.transpose(Y, (1, 0, 2)).reshape(Y.shape[1], -1) # (J, R1*R3) U2 np.linalg.svd(Y_mat, full_matricesFalse)[0][:, :ranks[1]] # 更新 U3: 固定 U1, U2投影后取 mode-3 展开 Y np.einsum(ijk,ia,jb-abk, X, U1, U2) # (R1, R2, K) Y_mat np.transpose(Y, (2, 0, 1)).reshape(Y.shape[2], -1) # (K, R1*R2) U3 np.linalg.svd(Y_mat, full_matricesFalse)[0][:, :ranks[2]] return U1, U2, U3每次更新时先把原张量投影到另外两个 mode 的低维空间得到低维张量 Y再对 Y 的当前 mode 展开做 SVD。以更新 U2 为例einsum 输出是 (R1, J, R3)必须先 np.transpose 把 J 挪到第一位再 reshape 成 (J, R1*R3) 的展开矩阵取左奇异向量的前 R2 列作为新的 U2。这个细节对应第 5 章的坑 2——展开前不转置迭代方向就错了。HOOI 的关键点有两个。第一初始化用第 2 章的 HOSVD 结果而不是随机初始化能显著加快收敛并避开差的局部最优。第二每轮迭代误差单调不增但可能停在局部最优所以工程上通常跑固定轮数后看重构误差曲线是否平稳。3.4 选型判断什么场景用 HOSVD什么场景上迭代我的经验是分三种情况。第一种做压缩或下游特征输入HOSVD 一次算完、因子正交不相关直接用它不需要迭代。第二种明确追求最小重构误差或者要做张量补全上 HOOI 或带约束的 Tucker 变体。第三种数据规模大、实时性要求高HOSVD 的逐 mode 分解可以按需增量计算比迭代方法更好落地。另外注意如果你在文献里看到“HOSVD Tucker 分解”的说法不要觉得矛盾。严格说是“HOSVD 是 Tucker 分解的一种正交约束实现”大部分工程实现里跑的就是这个特例。需要更强近似能力时在 HOSVD 初始化基础上升级到 HOOI 即可这条路径是连贯的。4. 用 NumPy 实现三阶 HOSVD完整代码与秩参数调试4.1 合成一个“真秩已知”的基准张量验证一个分解实现是否写对最好的方式是用“你知道答案”的数据先造一个真秩为 (R1, R2, R3) 的张量再加一点噪声然后让 HOSVD 去恢复它。如果恢复误差接近噪声水平说明分解逻辑正确。import numpy as np np.random.seed(42) I, J, K 40, 50, 60 # 原始张量三个维度 R1t, R2t, R3t 3, 3, 3 # 真实的秩 # 随机生成三个因子矩阵和一个核心张量 A np.random.randn(I, R1t) B np.random.randn(J, R2t) C np.random.randn(K, R3t) G0 np.random.randn(R1t, R2t, R3t) # 用 Tucker 正向来合成干净张量 X_clean np.einsum(abc,ia,jb,kc-ijk, G0, A, B, C) # 加噪声模拟观测数据 noise np.random.randn(I, J, K) * 0.05 X X_clean noise这里的关键是 einsum 的维度对应关系abc,ia,jb,kc-ijk 中G0 的三维 a、b、c 分别与 A、B、C 的第二维配对求积输出张量三个维度 i、j、k 分别来自 A、B、C 的第一维。这样生成的 X 在无噪声时严格是秩 (3,3,3) 的张量加噪后近似低秩。用这个数据测 HOSVD如果截断秩设为 (3,3,3)重构相对误差应该和噪声水平一个量级如果误差大得离谱说明代码有 bug。4.2 核心实现mode_n_unfold、hosvd 与 reconstruct这份源码包里的核心代码就是下面这套我把 mode_n_unfold、hosvd、reconstruct 三个函数整理成独立模块输入任意三阶 numpy 数组即可直接调用。def mode_n_unfold(X, n): 三阶张量沿 mode n 展开为矩阵 perm [n] [i for i in range(X.ndim) if i ! n] X_perm np.transpose(X, perm) rows X.shape[n] cols int(np.prod(X.shape) // rows) return X_perm.reshape(rows, cols) def hosvd(X, ranks): 三阶张量 HOSVD 分解。 ranks: (R1, R2, R3) 三个 mode 的截断秩 返回: (核心张量 G, 因子矩阵列表 [U1, U2, U3]) factors [] for n, r in enumerate(ranks): X_n mode_n_unfold(X, n) U, _, _ np.linalg.svd(X_n, full_matricesFalse) factors.append(U[:, :r]) # 取前 r 个左奇异向量 U1, U2, U3 factors G np.einsum(ijk,ia,jb,kc-abc, X, U1, U2, U3) return G, factors def reconstruct(G, factors): 由核心张量和因子矩阵重构原始形状的张量 U1, U2, U3 factors return np.einsum(abc,ia,jb,kc-ijk, G, U1, U2, U3)逐行说明。hosvd 里循环三次每次都做同一件事把 X 按对应 mode 展开成矩阵、做 SVD、取左奇异向量的前 r 列。U[:, :r] 的语义是保留行不变当前 mode 长度 I、J 或 K只压缩方向基的个数。之后 einsum 一次完成三路投影得到形状 (R1, R2, R3) 的核心张量。重构函数是对称操作把核心张量沿三个 mode 分别乘以对应因子矩阵形状还原回 (I, J, K)。参数上ranks 三个值独立指定互不影响。如果某个 mode 不想压缩把对应 r 设为该维度原长即可例如 ranks(I, 10, 10) 表示只压缩后两个 mode。要注意 r 必须小于等于展开矩阵的秩 min(I, J*K)实际使用时通常远小于这个上限。4.3 重构误差用 Frobenius 范数验证分解对不对最终看重构误差。相对误差的定义是 ||X - X_rec||_F / ||X||_F其中 ||·||_F 是张量所有元素平方和再开方。def rel_error(X, X_rec): 相对 Frobenius 范数误差 return np.linalg.norm(X - X_rec) / np.linalg.norm(X) G, factors hosvd(X, (3, 3, 3)) X_rec reconstruct(G, factors) print(相对误差:, rel_error(X, X_rec))提示ranks 里任何一个值超过展开矩阵的秩时SVD 会返回形状不匹配的 U报错位置通常在 np.linalg.svd 结果切片那行先检查 ranks 是否小于 min(mode_len, 其余维度乘积)。逻辑很简单先分解再重构比较差异。由于合成数据的噪声标准差是 0.05而信号能量来自三个因子矩阵的随机高斯值相对误差会接近噪声能量与信号能量之比。如果 ranks 设成真实秩 (3,3,3)误差大约是噪声水平如果设成 (2,2,2)误差会明显变大。建议实际跑一遍把 (3,3,3) 换成 (2,2,2)、(5,5,5) 各看一次对 HOSVD 的能力边界会有很直观的感受。4.4 秩参数调试能量比、维度表与搜索顺序HOSVD 的三阶分解里秩 (R1, R2, R3) 是唯一的超参数但它是三个独立的值。调参顺序我一般分两步走。第一步用奇异值能量比确定每个 mode 的秩上界。对第 n 个展开矩阵做完整 SVD 后看奇异值的累积能量什么时候超过 0.99def energy_rank(X, n, threshold0.99): 返回第 n 个 mode 展开矩阵达到阈值所需的最小秩 X_n mode_n_unfold(X, n) _, S, _ np.linalg.svd(X_n, full_matricesFalse) cum np.cumsum(S**2) total cum[-1] return int(np.argmax(cum threshold * total) 1)S 是奇异值数组S**2 是每个方向的能量贡献cumsum 得到累积能量argmax 找到第一个超过阈值的下标。注意返回的是秩不是能量比例threshold 设 0.99 表示保留 99% 能量。实际数据里 threshold 取 0.95 到 0.999 之间都常见取决于你对压缩率的容忍度。第二步在三个上界确定的网格里做粗到细搜索。不要一上来就对每个 mode 从 1 扫到 I组合数量爆炸。先用大步长比如 3 或 5粗扫定位误差尖锐变化的区域再在突变点附近用小步长1精扫。这部分变尺度搜索的做法在第 6 章给出具体实现这里先记住结论能量比管上界误差曲线管最优秩两者结合才稳。5. 避坑手册HOSVD 落地最容易翻车的五个地方5.1 因子矩阵方向取反取成右奇异向量的结果现象SVD 返回三个值 (U, S, Vt)有人图省事直接用 Vt[:r, :] 当因子矩阵重构时报维度错误或者不报错但重构结果形状对、数值完全不对。原因numpy.linalg.svd 对展开矩阵 X_n 做分解后U 是左奇异向量、Vt 是右奇异向量的转置。展开矩阵的行是当前 mode列是其余 mode 的联合方向。左奇异向量张成行空间才是当前 mode 的基右奇异向量张成列空间属于另一个空间不能直接当因子。解决一律取 U[:, :r]。另外加一个自检分解后检查每个因子矩阵的 shape 是不是 (mode_len, r)对三阶张量就是 (I, R1)、(J, R2)、(K, R3)。但凡有一个 shape 不对先怀疑是不是取反了。5.2 mode-n 展开忘记 transpose列顺序错乱的隐性 bug现象mode-1 结果正常mode-2 或 mode-3 的重构误差总是很大。检查因子矩阵也有明显的能量分布但重构张量看起来像打过乱码。原因np.reshape 按 C 顺序读内存。三阶张量内存布局是 k 变化最快、j 次之、i 最慢。X.reshape(I, JK) 恰好对应 mode-1 展开但 X.reshape(J, IK) 不是 mode-2 展开——它把内存按 (i, k) 的顺序硬塞给了行和列列顺序没按标准展开定义。解决统一用 2.2 节的 mode_n_unfold 函数先 np.transpose 把目标轴挪到第一个位置再 reshape。自检方式对 mode-2 展开检查 X_unfold[0, 0] 是否等于 X[0, 0, 0]X_unfold[0, 1] 是否等于 X[0, 0, 1]如果是说明列顺序按 (i, k) 遍历没问题。5.3 能量比 99% 不等于重构最优现象三个 mode 都按能量比 0.99 截断后整体重构误差明显高于预期或者压缩率上去之后误差曲线出现跳变。原因截断 HOSVD 是逐 mode 独立截断。每个 mode 在它自己的展开矩阵意义下是最优截断但三个截断组合起来只能给出误差上界不是全局最小误差。矩阵 SVD 的 Eckart–Young 定理在三阶张量上没有直接推广。解决如果误差不可接受用第 3 章的 HOOI 在 HOSVD 初始化上继续迭代误差会单调下降。如果误差可接受就把 HOSVD 当作快速版分解来用。另外别拿能量比替代真实重构误差做判断能量比只能定上界。5.4 数据没归一化奇异值谱被量纲支配现象分解前没做预处理结果某个 mode 的奇异值第一个就占了总能量 99.9%能量比法选出的秩几乎是 1重构效果一塌糊涂。原因HOSVD 的奇异值来自展开矩阵的协方差结构。如果不同 mode 的数据量纲或方差差距很大比如图像像素值在 0~255而时间维度是毫秒级数值展开矩阵的奇异值会被量纲大的 mode 支配能量比失真。解决分解前先做标准化。常见做法是对每个 mode 的展开矩阵按行做中心化和缩放让各 mode 方差接近 1。注意标准化参数要保存下来重构时先反标准化再乘因子否则还原出来的数据量纲是错的。如果不想引入非线性变换也可以只做每个 mode 的全局均值和标准差缩放关键是不要让某个 mode 的量纲支配奇异值谱。5.5 核心张量不唯一符号与旋转自由度现象同一份数据两次分解得到的核心张量元素符号不同甚至数值有整体旋转差异。有人怀疑代码写错了或者下游模型效果不稳定。原因HOSVD 的因子矩阵列正交但正交基在符号和旋转上不唯一。因子矩阵每一列乘以 -1等价地核心张量在对应维度翻转符号重构结果完全不变。如果展开矩阵存在重复奇异值对应方向还允许在子空间里旋转。这是分解的固有自由度不是 bug。解决做特征提取或下游建模时不要直接解释核心张量单个元素的符号。要做可解释分析对核心张量或因子矩阵施加额外约束比如非负分解。做压缩和重构时这种不唯一性没有影响判定代码正确性的标准是重构误差而不是两次分解的元素级一致。6. 变尺度搜索定秩从奇异值谱到重构误差曲线的最后一公里6.1 能量比定上界拐点定候选energy_rank 给的是“达到 99% 能量所需的最小秩”它适合当上界不适合直接当最优秩。更稳的做法是画出每个 mode 展开矩阵的奇异值谱找拐点奇异值从陡峭下降转为平缓的位置对应信号的“有效秩”。拐点附近才是候选秩区间。上界和拐点结合能把三个 mode 的候选区间缩到很小范围。6.2 粗扫定位细扫定秩候选区间确定后用变尺度搜索代替全网格扫描。先用大步长3 或 5在每个区间里粗扫画出重构误差的曲线或等高线找到误差明显下降又趋于平稳的区域再在该区域用步长 1 精扫避免组合爆炸。def coarse_fine_search(hosvd_fn, X, bounds, coarse_step3): bounds: [(r1_min, r1_max), ...] 每个 mode 的秩范围 best None # 粗扫阶段 grids [range(lo, hi 1, coarse_step) for lo, hi in bounds] for r1 in grids[0]: for r2 in grids[1]: for r3 in grids[2]: G, factors hosvd_fn(X, (r1, r2, r3)) X_rec reconstruct(G, factors) err rel_error(X, X_rec) if best is None or err best[0]: best (err, (r1, r2, r3)) # 细扫阶段围绕粗扫最优值附近加减 2 的范围内精扫 fine_grids [range(max(1, v - 2), v 3, 1) for v in best[1]] for r1 in fine_grids[0]: for r2 in fine_grids[1]: for r3 in fine_grids[2]: G, factors hosvd_fn(X, (r1, r2, r3)) X_rec reconstruct(G, factors) err rel_error(X, X_rec) if err best[0]: best (err, (r1, r2, r3)) return best这段代码的逻辑很直白粗扫用大步长覆盖整个区间记录误差最小的秩组合细扫只在粗扫最优值附近两格范围内用小步长精扫。三层循环的复杂度从全域搜索的立方级降到了实际可用级别。注意 ranks 的值若小于真实有效秩误差会陡增大于有效秩误差只缓慢下降。所以最优组合通常落在误差曲线由陡变缓的拐点附近这正是变尺度搜索能捞到的地方。6.3 最终验证重构误差与核心张量能量集中度选完秩后最后做两个验证。第一个看相对重构误差是否接近噪声能量水平如果远高于说明秩选低了。第二个看核心张量的能量分布是否集中核心张量平方后按元素排序前 10% 的元素若能贡献 99% 以上的能量说明分解质量高如果能量散布在所有元素里说明秩选得过于宽松或数据结构不适合低秩分解。我最早做张量压缩的时候图省事直接按每个 mode 单独扫秩三个维度组合起来要跑上百次分解后来改成先能量比定上界、再变尺度搜索的做法一次任务从十几分钟压缩到几十秒而且结果更稳定。从那以后我每次做 HOSVD 前都强制走一遍“奇异值谱看拐点、粗扫找区域、细扫定秩”的流程省掉的返工时间远超那几分钟搜索成本。这份源码包把这条链路完整串起来了基准张量生成、HOSVD、HOOI、变尺度搜索定秩拿自己的三阶数据替换 X 就能跑。希望帮到你。本文还有配套的精品资源点击获取